diff --git a/README.md b/README.md
index 0cc7664..bf1b82c 100644
--- a/README.md
+++ b/README.md
@@ -5,11 +5,12 @@
VidBee
-
-
-
-
-
+
+
+
+
+
+
diff --git a/package.json b/package.json
index 6c856ae..1267114 100644
--- a/package.json
+++ b/package.json
@@ -15,10 +15,10 @@
"build": "electron-vite build",
"setup": "node scripts/setup-dev-binaries.js",
"postinstall": "node scripts/setup-dev-binaries.js && electron-builder install-app-deps",
- "build:unpack": "pnpm run build && electron-builder --dir",
- "build:win": "node scripts/check-ytdlp.js win && pnpm run build && electron-builder --win",
- "build:mac": "node scripts/check-ytdlp.js mac && pnpm run build && electron-builder --mac --x64 --arm64",
- "build:linux": "node scripts/check-ytdlp.js linux && pnpm run build && electron-builder --linux",
+ "build:unpack": "pnpm run setup && pnpm run build && electron-builder --dir",
+ "build:win": "pnpm run setup && node scripts/check-ytdlp.js win && pnpm run build && electron-builder --win",
+ "build:mac": "pnpm run setup && node scripts/check-ytdlp.js mac && pnpm run build && electron-builder --mac --x64 --arm64",
+ "build:linux": "pnpm run setup && node scripts/check-ytdlp.js linux && pnpm run build && electron-builder --linux",
"release": "git checkout main && git pull && pnpm run check && bumpp",
"db:generate": "drizzle-kit generate"
},
diff --git a/resources/.gitignore b/resources/.gitignore
index 6d02c3a..6360655 100644
--- a/resources/.gitignore
+++ b/resources/.gitignore
@@ -6,6 +6,8 @@ ffmpeg.exe
ffmpeg_macos
ffmpeg_linux
ffmpeg
+deno.exe
+deno
# But keep the README
!README.md
diff --git a/resources/README.md b/resources/README.md
index 74d2506..3d8c736 100644
--- a/resources/README.md
+++ b/resources/README.md
@@ -69,3 +69,23 @@ ffmpeg is required for merging audio/video streams and audio extraction. Bundle
- Bundled binaries are required for Windows builds. On macOS/Linux the app can also use ffmpeg/yt-dlp from the system PATH.
- You can override the lookup paths via the `YTDLP_PATH` or `FFMPEG_PATH` environment variables if you prefer custom locations.
- File sizes: ~10-15 MB per yt-dlp binary, ~40-80 MB per ffmpeg binary
+
+## JS Runtime (Deno)
+
+yt-dlp uses an external JS runtime (Deno by default) for some extractors. Bundle a Deno binary so the app can run without system dependencies.
+
+### Required Files
+
+1. **Windows**: `deno.exe`
+2. **macOS**: `deno`
+3. **Linux**: `deno`
+
+### How to Download
+
+- Visit:
+- Download the matching platform archive and extract the `deno` (or `deno.exe`) binary into `resources/`.
+- On macOS/Linux ensure the file is executable: `chmod +x resources/deno`
+
+### Note
+
+- You can override the runtime path via `YTDLP_JS_RUNTIME_PATH` if needed.
diff --git a/scripts/check-ytdlp.js b/scripts/check-ytdlp.js
index ee9882f..4c966df 100644
--- a/scripts/check-ytdlp.js
+++ b/scripts/check-ytdlp.js
@@ -44,6 +44,17 @@ const binaries = [
linux: 'https://ffmpeg.org/download.html',
mac: 'https://github.com/eko5624/mpv-mac/releases/latest'
}
+ },
+ {
+ label: 'deno',
+ filenameMap: {
+ win: 'deno.exe',
+ mac: 'deno',
+ linux: 'deno'
+ },
+ help: {
+ default: 'https://github.com/denoland/deno/releases/latest'
+ }
}
]
diff --git a/scripts/setup-dev-binaries.js b/scripts/setup-dev-binaries.js
index 718f870..776a6f8 100755
--- a/scripts/setup-dev-binaries.js
+++ b/scripts/setup-dev-binaries.js
@@ -15,6 +15,7 @@ const http = require('node:http')
// Configuration
const RESOURCES_DIR = path.join(__dirname, '..', 'resources')
const YTDLP_BASE_URL = 'https://github.com/yt-dlp/yt-dlp/releases/latest/download'
+const DENO_BASE_URL = 'https://github.com/denoland/deno/releases/latest/download'
const GITHUB_TOKEN =
process.env.GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_API_TOKEN
@@ -257,6 +258,32 @@ function fileExists(filePath) {
return fs.existsSync(filePath)
}
+function getDenoAssetName(platform, arch) {
+ if (platform === 'win32') {
+ if (arch === 'arm64') {
+ return 'deno-aarch64-pc-windows-msvc.zip'
+ }
+ return 'deno-x86_64-pc-windows-msvc.zip'
+ }
+ if (platform === 'darwin') {
+ if (arch === 'arm64') {
+ return 'deno-aarch64-apple-darwin.zip'
+ }
+ return 'deno-x86_64-apple-darwin.zip'
+ }
+ if (platform === 'linux') {
+ if (arch === 'arm64') {
+ return 'deno-aarch64-unknown-linux-gnu.zip'
+ }
+ return 'deno-x86_64-unknown-linux-gnu.zip'
+ }
+ return null
+}
+
+function getDenoOutputName(platform) {
+ return platform === 'win32' ? 'deno.exe' : 'deno'
+}
+
// Main download functions
async function downloadYtDlp(config) {
const { asset, output } = config.ytdlp
@@ -447,6 +474,52 @@ async function downloadFfmpegLinux(config) {
}
}
+async function downloadDenoRuntime() {
+ const platform = os.platform()
+ const arch = os.arch()
+ const assetName = getDenoAssetName(platform, arch)
+
+ if (!assetName) {
+ log(`Skipping Deno runtime: unsupported platform/arch ${platform}/${arch}`, 'warn')
+ return
+ }
+
+ const outputName = getDenoOutputName(platform)
+ const outputPath = path.join(RESOURCES_DIR, outputName)
+
+ if (fileExists(outputPath)) {
+ log(`${outputName} already exists, skipping download`, 'info')
+ return
+ }
+
+ log(`Downloading Deno runtime (${platform}/${arch})...`, 'download')
+ const tempZip = path.join(RESOURCES_DIR, 'deno-temp.zip')
+ const extractDir = path.join(RESOURCES_DIR, 'deno-temp')
+ const downloadUrl = `${DENO_BASE_URL}/${assetName}`
+
+ try {
+ await downloadFile(downloadUrl, tempZip)
+ log('Extracting Deno runtime...', 'info')
+ extractZip(tempZip, extractDir)
+
+ const sourcePath = path.join(extractDir, outputName)
+ if (!fileExists(sourcePath)) {
+ throw new Error(`Deno binary not found at ${sourcePath}`)
+ }
+
+ fs.copyFileSync(sourcePath, outputPath)
+ setExecutable(outputPath)
+ log(`Downloaded ${outputName} successfully`, 'success')
+
+ fs.unlinkSync(tempZip)
+ fs.rmSync(extractDir, { recursive: true, force: true })
+ } catch (error) {
+ if (fs.existsSync(tempZip)) fs.unlinkSync(tempZip)
+ if (fs.existsSync(extractDir)) fs.rmSync(extractDir, { recursive: true, force: true })
+ throw error
+ }
+}
+
// Main setup function
async function setup() {
const platform = os.platform()
@@ -464,6 +537,9 @@ async function setup() {
// Download yt-dlp
await downloadYtDlp(config)
+ // Download JS runtime (Deno)
+ await downloadDenoRuntime()
+
// Download ffmpeg
if (platform === 'win32') {
await downloadFfmpegWindows(config)
diff --git a/src/main/config/logger-config.ts b/src/main/config/logger-config.ts
index 2ccd69d..341b2e4 100644
--- a/src/main/config/logger-config.ts
+++ b/src/main/config/logger-config.ts
@@ -5,15 +5,6 @@ import log from 'electron-log/main'
* Set log format, file path, transport methods, etc.
*/
export function configureLogger() {
- // Configure console output format - support colors and scope, time in gray
- log.transports.console.format = '%c{h}:{i}:{s}%c [{level}]{scope} {text}'
-
- // Enable console colors
- log.transports.console.useStyles = true
-
- // Configure file output format - include scope information
- log.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}] [{level}] {scope} {text}'
-
// Set log levels
// Development: show all logs
// Production: show info level and above only
diff --git a/src/main/download-engine/args-builder.ts b/src/main/download-engine/args-builder.ts
index e9a33b5..6dd033d 100644
--- a/src/main/download-engine/args-builder.ts
+++ b/src/main/download-engine/args-builder.ts
@@ -61,7 +61,8 @@ export const resolveAudioFormatSelector = (options: DownloadOptions): string =>
export const buildDownloadArgs = (
options: DownloadOptions,
downloadPath: string,
- settings: AppSettings
+ settings: AppSettings,
+ jsRuntimeArgs: string[] = []
): string[] => {
const args: string[] = ['--no-playlist', '--embed-chapters', '--no-mtime']
@@ -130,6 +131,10 @@ export const buildDownloadArgs = (
args.push('--config-location', configPath)
}
+ if (jsRuntimeArgs.length > 0) {
+ args.push(...jsRuntimeArgs)
+ }
+
args.push(options.url)
return args
diff --git a/src/main/ipc/services/app-service.ts b/src/main/ipc/services/app-service.ts
index 5325737..6b418ac 100644
--- a/src/main/ipc/services/app-service.ts
+++ b/src/main/ipc/services/app-service.ts
@@ -1,6 +1,7 @@
import os from 'node:os'
import { app, BrowserWindow, dialog } from 'electron'
import { type IpcContext, IpcMethod, IpcService } from 'electron-ipc-decorator'
+import { scopedLoggers } from '../../utils/logger'
class AppService extends IpcService {
static readonly groupName = 'app'
@@ -48,7 +49,7 @@ class AppService extends IpcService {
const base64 = buffer.toString('base64')
return `data:${contentType};base64,${base64}`
} catch (error) {
- console.error('Failed to fetch site icon:', error)
+ scopedLoggers.system.error('Failed to fetch site icon:', error)
return null
}
}
diff --git a/src/main/ipc/services/file-system-service.ts b/src/main/ipc/services/file-system-service.ts
index 2f47464..b345f76 100644
--- a/src/main/ipc/services/file-system-service.ts
+++ b/src/main/ipc/services/file-system-service.ts
@@ -6,6 +6,7 @@ import { pathToFileURL } from 'node:url'
import { promisify } from 'node:util'
import { clipboard, dialog, shell } from 'electron'
import { type IpcContext, IpcMethod, IpcService } from 'electron-ipc-decorator'
+import { scopedLoggers } from '../../utils/logger'
const execFileAsync = promisify(execFile)
@@ -49,7 +50,10 @@ class FileSystemService extends IpcService {
return xdgPath
}
} catch (error) {
- console.warn('Unable to resolve XDG download directory, falling back to default:', error)
+ scopedLoggers.system.warn(
+ 'Unable to resolve XDG download directory, falling back to default:',
+ error
+ )
}
}
@@ -75,7 +79,7 @@ class FileSystemService extends IpcService {
if (stats?.isDirectory()) {
const result = await shell.openPath(normalizedPath)
if (result) {
- console.error('Failed to open directory:', result)
+ scopedLoggers.system.error('Failed to open directory:', result)
return false
}
return true
@@ -88,16 +92,16 @@ class FileSystemService extends IpcService {
if (parentStats?.isDirectory()) {
const result = await shell.openPath(parentDirectory)
if (result) {
- console.error('Failed to open parent directory:', result)
+ scopedLoggers.system.error('Failed to open parent directory:', result)
return false
}
return true
}
- console.error('File or directory does not exist:', normalizedPath)
+ scopedLoggers.system.error('File or directory does not exist:', normalizedPath)
return false
} catch (error) {
- console.error('Failed to open file location:', error)
+ scopedLoggers.system.error('Failed to open file location:', error)
return false
}
}
@@ -122,7 +126,7 @@ class FileSystemService extends IpcService {
return true
} catch (error) {
- console.error('Failed to copy file to clipboard:', error)
+ scopedLoggers.system.error('Failed to copy file to clipboard:', error)
return false
}
}
@@ -150,7 +154,7 @@ class FileSystemService extends IpcService {
await shell.openExternal(url)
return true
} catch (error) {
- console.error('Failed to open external URL:', error)
+ scopedLoggers.system.error('Failed to open external URL:', error)
return false
}
}
@@ -166,7 +170,10 @@ class FileSystemService extends IpcService {
])
return
} catch (error) {
- console.error('PowerShell clipboard copy failed, falling back to manual buffer:', error)
+ scopedLoggers.system.error(
+ 'PowerShell clipboard copy failed, falling back to manual buffer:',
+ error
+ )
}
const winPath = resolvedPath.replace(/\//g, '\\')
@@ -194,7 +201,10 @@ class FileSystemService extends IpcService {
await execFileAsync('osascript', ['-e', `set the clipboard to (POSIX file "${escaped}")`])
return
} catch (error) {
- console.error('osascript clipboard copy failed, falling back to manual buffer:', error)
+ scopedLoggers.system.error(
+ 'osascript clipboard copy failed, falling back to manual buffer:',
+ error
+ )
}
const entries = [
@@ -243,7 +253,7 @@ class FileSystemService extends IpcService {
return stats?.isFile() ?? false
} catch (error) {
- console.error('Failed to check file existence:', error)
+ scopedLoggers.system.error('Failed to check file existence:', error)
return false
}
}
@@ -295,7 +305,7 @@ class FileSystemService extends IpcService {
return false
} catch (error) {
- console.error('Failed to delete file:', error)
+ scopedLoggers.system.error('Failed to delete file:', error)
return false
}
}
diff --git a/src/main/lib/download-engine.ts b/src/main/lib/download-engine.ts
index 0b54e33..7e57370 100644
--- a/src/main/lib/download-engine.ts
+++ b/src/main/lib/download-engine.ts
@@ -145,6 +145,13 @@ const resolveHistoryDownloadPath = (
return path.join(basePath, templateDir)
}
+const appendJsRuntimeArgs = (args: string[]): void => {
+ const runtimeArgs = ytdlpManager.getJsRuntimeArgs()
+ if (runtimeArgs.length > 0) {
+ args.push(...runtimeArgs)
+ }
+}
+
class DownloadEngine extends EventEmitter {
private activeDownloads: Map = new Map()
private queue: DownloadQueue
@@ -194,6 +201,7 @@ class DownloadEngine extends EventEmitter {
args.push('--config-location', configPath)
}
+ appendJsRuntimeArgs(args)
args.push(url)
return new Promise((resolve, reject) => {
@@ -290,6 +298,7 @@ class DownloadEngine extends EventEmitter {
args.push('--config-location', configPath)
}
+ appendJsRuntimeArgs(args)
args.push(url)
type RawPlaylistEntry = {
@@ -511,7 +520,7 @@ class DownloadEngine extends EventEmitter {
startDownload(id: string, options: DownloadOptions): void {
if (this.activeDownloads.has(id)) {
- console.warn(`Download ${id} is already active`)
+ scopedLoggers.engine.warn(`Download ${id} is already active`)
return
}
@@ -644,7 +653,12 @@ class DownloadEngine extends EventEmitter {
return true
}
- const args = buildDownloadArgs(options, resolvedDownloadPath, settings)
+ const args = buildDownloadArgs(
+ options,
+ resolvedDownloadPath,
+ settings,
+ ytdlpManager.getJsRuntimeArgs()
+ )
const captureOutputPath = (rawPath: string | undefined): void => {
if (!rawPath) {
diff --git a/src/main/lib/ffmpeg-manager.ts b/src/main/lib/ffmpeg-manager.ts
index 08396c6..4fd181f 100644
--- a/src/main/lib/ffmpeg-manager.ts
+++ b/src/main/lib/ffmpeg-manager.ts
@@ -2,13 +2,14 @@ import { execSync } from 'node:child_process'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
+import { scopedLoggers } from '../utils/logger'
class FfmpegManager {
private ffmpegPath: string | null = null
async initialize(): Promise {
this.ffmpegPath = await this.findFfmpegBinary()
- console.log('ffmpeg initialized at:', this.ffmpegPath)
+ scopedLoggers.engine.info('ffmpeg initialized at:', this.ffmpegPath)
}
getPath(): string {
@@ -30,7 +31,7 @@ class FfmpegManager {
const resourceCandidates: string[] = []
if (process.env.FFMPEG_PATH && fs.existsSync(process.env.FFMPEG_PATH)) {
- console.log('Using ffmpeg from FFMPEG_PATH:', process.env.FFMPEG_PATH)
+ scopedLoggers.engine.info('Using ffmpeg from FFMPEG_PATH:', process.env.FFMPEG_PATH)
return process.env.FFMPEG_PATH
}
@@ -50,10 +51,13 @@ class FfmpegManager {
try {
fs.chmodSync(fullPath, 0o755)
} catch (error) {
- console.warn('Failed to set executable permission on ffmpeg binary:', error)
+ scopedLoggers.engine.warn(
+ 'Failed to set executable permission on ffmpeg binary:',
+ error
+ )
}
}
- console.log('Using bundled ffmpeg:', fullPath)
+ scopedLoggers.engine.info('Using bundled ffmpeg:', fullPath)
return fullPath
}
}
@@ -62,7 +66,7 @@ class FfmpegManager {
const commonPaths = ['/opt/homebrew/bin/ffmpeg', '/usr/local/bin/ffmpeg']
for (const candidate of commonPaths) {
if (fs.existsSync(candidate)) {
- console.log('Using system ffmpeg:', candidate)
+ scopedLoggers.engine.info('Using system ffmpeg:', candidate)
return candidate
}
}
@@ -72,7 +76,7 @@ class FfmpegManager {
try {
const systemPath = execSync('which ffmpeg').toString().trim()
if (systemPath && fs.existsSync(systemPath)) {
- console.log('Using system ffmpeg:', systemPath)
+ scopedLoggers.engine.info('Using system ffmpeg:', systemPath)
return systemPath
}
} catch (_error) {
@@ -84,7 +88,7 @@ class FfmpegManager {
try {
const output = execSync('where ffmpeg').toString().split(/\r?\n/)[0]
if (output && fs.existsSync(output)) {
- console.log('Using system ffmpeg:', output)
+ scopedLoggers.engine.info('Using system ffmpeg:', output)
return output
}
} catch (_error) {
diff --git a/src/main/lib/thumbnail-cache.ts b/src/main/lib/thumbnail-cache.ts
index 0a646b7..93e0e60 100644
--- a/src/main/lib/thumbnail-cache.ts
+++ b/src/main/lib/thumbnail-cache.ts
@@ -4,6 +4,7 @@ import fsPromises from 'node:fs/promises'
import path from 'node:path'
import { APP_PROTOCOL_SCHEME } from '@shared/constants'
import { app } from 'electron'
+import { scopedLoggers } from '../utils/logger'
const SUPPORTED_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif'])
@@ -81,7 +82,7 @@ export class ThumbnailCache {
await fsPromises.writeFile(finalPath, buffer)
return this.toAppProtocolUrl(finalPath)
} catch (error) {
- console.error('Failed to cache thumbnail:', error)
+ scopedLoggers.thumbnail.error('Failed to cache thumbnail:', error)
return null
}
}
diff --git a/src/main/lib/ytdlp-manager.ts b/src/main/lib/ytdlp-manager.ts
index 62ed336..3e04b14 100644
--- a/src/main/lib/ytdlp-manager.ts
+++ b/src/main/lib/ytdlp-manager.ts
@@ -3,6 +3,7 @@ import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import type YTDlpWrap from 'yt-dlp-wrap-plus'
+import { scopedLoggers } from '../utils/logger'
// Use require for yt-dlp-wrap-plus to handle CommonJS/ESM compatibility
const YTDlpWrapModule = require('yt-dlp-wrap-plus')
@@ -12,11 +13,13 @@ type YTDlpWrapInstance = InstanceType
class YtDlpManager {
private ytdlpPath: string | null = null
private ytdlpInstance: YTDlpWrapInstance | null = null
+ private jsRuntimeArgs: string[] = []
async initialize(): Promise {
this.ytdlpPath = await this.findOrDownloadYtDlp()
this.ytdlpInstance = new YTDlpWrapCtor(this.ytdlpPath)
- console.log('yt-dlp initialized at:', this.ytdlpPath)
+ this.jsRuntimeArgs = this.resolveJsRuntimeArgs()
+ scopedLoggers.engine.info('yt-dlp initialized at:', this.ytdlpPath)
}
getInstance(): YTDlpWrapInstance {
@@ -33,6 +36,10 @@ class YtDlpManager {
return this.ytdlpPath
}
+ getJsRuntimeArgs(): string[] {
+ return [...this.jsRuntimeArgs]
+ }
+
private getResourcesPath(): string {
// In development, read from project root's resources
if (process.env.NODE_ENV === 'development') {
@@ -57,7 +64,7 @@ class YtDlpManager {
// Check environment variable first
if (process.env.YTDLP_PATH && fs.existsSync(process.env.YTDLP_PATH)) {
- console.log('Using yt-dlp from YTDLP_PATH:', process.env.YTDLP_PATH)
+ scopedLoggers.engine.info('Using yt-dlp from YTDLP_PATH:', process.env.YTDLP_PATH)
return process.env.YTDLP_PATH
}
@@ -65,13 +72,13 @@ class YtDlpManager {
const resourcesPath = this.getResourcesPath()
const bundledPath = path.join(resourcesPath, bundledName)
if (fs.existsSync(bundledPath)) {
- console.log('Using bundled yt-dlp:', bundledPath)
+ scopedLoggers.engine.info('Using bundled yt-dlp:', bundledPath)
// Make executable on Unix-like systems if needed
if (platform !== 'win32') {
try {
fs.chmodSync(bundledPath, 0o755)
} catch (error) {
- console.warn('Failed to set executable permission:', error)
+ scopedLoggers.engine.warn('Failed to set executable permission:', error)
}
}
return bundledPath
@@ -82,7 +89,7 @@ class YtDlpManager {
const possiblePaths = ['/opt/homebrew/bin/yt-dlp', '/usr/local/bin/yt-dlp']
for (const p of possiblePaths) {
if (fs.existsSync(p)) {
- console.log('Using system yt-dlp:', p)
+ scopedLoggers.engine.info('Using system yt-dlp:', p)
return p
}
}
@@ -93,7 +100,7 @@ class YtDlpManager {
try {
const systemPath = execSync('which yt-dlp').toString().trim()
if (systemPath && fs.existsSync(systemPath)) {
- console.log('Using system yt-dlp:', systemPath)
+ scopedLoggers.engine.info('Using system yt-dlp:', systemPath)
return systemPath
}
} catch (_error) {
@@ -115,6 +122,92 @@ class YtDlpManager {
)
}
+ private resolveJsRuntimeArgs(): string[] {
+ const runtime = (process.env.YTDLP_JS_RUNTIME || 'deno').trim()
+ if (!runtime || runtime === 'none') {
+ return []
+ }
+
+ const runtimePath = this.resolveJsRuntimePath(runtime)
+ if (runtimePath) {
+ return ['--js-runtimes', `${runtime}:${runtimePath}`]
+ }
+
+ if (process.env.YTDLP_JS_RUNTIME) {
+ scopedLoggers.engine.warn(
+ `Requested JS runtime "${runtime}" was not found. Falling back to yt-dlp default detection.`
+ )
+ } else {
+ scopedLoggers.engine.warn(
+ 'JS runtime not found. YouTube support may be limited without an external JS runtime.'
+ )
+ }
+
+ return process.env.YTDLP_JS_RUNTIME ? ['--js-runtimes', runtime] : []
+ }
+
+ private resolveJsRuntimePath(runtime: string): string | null {
+ const envPath = process.env.YTDLP_JS_RUNTIME_PATH?.trim()
+ if (envPath && fs.existsSync(envPath)) {
+ scopedLoggers.engine.info('Using JS runtime from YTDLP_JS_RUNTIME_PATH:', envPath)
+ return envPath
+ }
+
+ const platform = os.platform()
+ const resourcesPath = this.getResourcesPath()
+ const resourceCandidates: string[] = []
+
+ if (runtime === 'deno') {
+ resourceCandidates.push(platform === 'win32' ? 'deno.exe' : 'deno')
+ } else if (runtime === 'node') {
+ resourceCandidates.push(platform === 'win32' ? 'node.exe' : 'node')
+ } else if (runtime === 'bun') {
+ resourceCandidates.push(platform === 'win32' ? 'bun.exe' : 'bun')
+ } else if (runtime === 'quickjs') {
+ resourceCandidates.push(platform === 'win32' ? 'qjs.exe' : 'qjs')
+ } else {
+ resourceCandidates.push(runtime)
+ if (platform === 'win32' && !runtime.endsWith('.exe')) {
+ resourceCandidates.push(`${runtime}.exe`)
+ }
+ }
+
+ for (const candidate of resourceCandidates) {
+ const fullPath = path.join(resourcesPath, candidate)
+ if (fs.existsSync(fullPath)) {
+ if (platform !== 'win32') {
+ try {
+ fs.chmodSync(fullPath, 0o755)
+ } catch (error) {
+ scopedLoggers.engine.warn('Failed to set executable permission on JS runtime:', error)
+ }
+ }
+ scopedLoggers.engine.info('Using bundled JS runtime:', fullPath)
+ return fullPath
+ }
+ }
+
+ try {
+ if (platform === 'win32') {
+ const output = execSync(`where ${runtime}`).toString().split(/\r?\n/)[0]
+ if (output && fs.existsSync(output)) {
+ scopedLoggers.engine.info('Using system JS runtime:', output)
+ return output
+ }
+ } else {
+ const systemPath = execSync(`which ${runtime}`).toString().trim()
+ if (systemPath && fs.existsSync(systemPath)) {
+ scopedLoggers.engine.info('Using system JS runtime:', systemPath)
+ return systemPath
+ }
+ }
+ } catch (_error) {
+ // Runtime not found in PATH
+ }
+
+ return null
+ }
+
// Removed runtime download/update to avoid network dependency in production builds
}
diff --git a/src/main/settings.ts b/src/main/settings.ts
index ebb5ee6..cebf689 100644
--- a/src/main/settings.ts
+++ b/src/main/settings.ts
@@ -3,6 +3,7 @@ import os from 'node:os'
import path from 'node:path'
import type { AppSettings } from '../shared/types'
import { defaultSettings } from '../shared/types'
+import { scopedLoggers } from './utils/logger'
// Use require for electron-store to avoid CommonJS/ESM issues
const ElectronStore = require('electron-store')
@@ -14,7 +15,7 @@ const ensureDirectoryExists = (dir: string) => {
try {
fs.mkdirSync(dir, { recursive: true })
} catch (error) {
- console.error('Failed to ensure download directory:', error)
+ scopedLoggers.system.error('Failed to ensure download directory:', error)
}
}
@@ -85,7 +86,7 @@ class SettingsManager {
}
ensureDirectoryExists(normalizedDownloadPath)
} catch (error) {
- console.error('Failed to verify download directory:', error)
+ scopedLoggers.system.error('Failed to verify download directory:', error)
}
}
}