auto-git:

[change] package.json
 [change] run.sh
 [change] src-tauri/tauri.conf.json
 [unlink] electron/main.cjs
 [unlink] electron/preload.cjs
 [unlink] scripts/run-electron-dev.cjs
This commit is contained in:
2026-05-06 05:58:13 +02:00
parent e61c91b69e
commit b085239fe5
6 changed files with 10 additions and 861 deletions

View File

@@ -1,740 +0,0 @@
const { app, BrowserWindow, Menu, dialog, ipcMain, shell } = require('electron')
const path = require('path')
const { is } = require('@electron-toolkit/utils')
const fs = require('fs')
const { execFile } = require('child_process')
const { promisify } = require('util')
let mainWindow
let settingsWindow = null
const execFileAsync = promisify(execFile)
const DEFAULT_BACKEND_API_URL = 'http://127.0.0.1:8000'
const DEFAULT_OLLAMA_API_URL = 'http://127.0.0.1:11434'
const DEFAULT_EMBED_MODEL = 'nomic-embed-text:latest'
const DEFAULT_RERANK_MODEL = DEFAULT_EMBED_MODEL
const DEFAULT_TRANSCRIPTION_MODEL = 'base'
const BGE_EMBED_MODEL = 'bge-m3:latest'
const REPO_ROOT = path.resolve(__dirname, '..')
const UPDATE_REMOTE_URL = 'https://giers10.uber.space/giers10/Heimgeist.git'
const UPDATE_BRANCH = 'master'
const GIT_ENV = { ...process.env, GIT_TERMINAL_PROMPT: '0' }
const DEV_WRAPPER_RELAUNCH_CODE = Number(process.env.HEIMGEIST_DEV_RELAUNCH_CODE || 75)
const settingsFilePath = process.env.HEIMGEIST_SETTINGS_FILE || path.join(app.getPath('userData'), 'settings.json')
let appSettings = {}
let lastUpdateCheckResult = null
let activeUpdateCheck = null
const DEFAULT_UI_SCALE = 1
const MIN_UI_SCALE = 0.7
const MAX_UI_SCALE = 1.3
const DEFAULT_OPEN_DEVTOOLS_ON_STARTUP = false
const DEFAULT_AUDIO_INPUT_ENABLED = true
const DEFAULT_AUDIO_INPUT_DEVICE_ID = ''
const DEFAULT_AUDIO_INPUT_LANGUAGE = ''
const CHANGELOG_PAGE_SIZE = 50
const CHANGELOG_MESSAGE_MAX_LENGTH = 500
const defaultSettings = {
backendApiUrl: DEFAULT_BACKEND_API_URL,
ollamaApiUrl: DEFAULT_OLLAMA_API_URL,
chatModel: 'llama3',
visionModel: '',
embedModel: DEFAULT_EMBED_MODEL,
rerankModel: DEFAULT_RERANK_MODEL,
transcriptionModel: DEFAULT_TRANSCRIPTION_MODEL,
colorScheme: 'Default',
uiScale: DEFAULT_UI_SCALE,
openDevToolsOnStartup: DEFAULT_OPEN_DEVTOOLS_ON_STARTUP,
audioInputEnabled: DEFAULT_AUDIO_INPUT_ENABLED,
audioInputDeviceId: DEFAULT_AUDIO_INPUT_DEVICE_ID,
audioInputLanguage: DEFAULT_AUDIO_INPUT_LANGUAGE,
}
function normalizeEmbedModel(value) {
const trimmed = String(value || '').trim()
if (!trimmed) {
return DEFAULT_EMBED_MODEL
}
const lowered = trimmed.toLowerCase()
if (lowered === 'bge' || lowered === 'bge-m3' || lowered === BGE_EMBED_MODEL) {
return BGE_EMBED_MODEL
}
if (lowered === 'nomic' || lowered === 'nomic-embed-text' || lowered === DEFAULT_EMBED_MODEL) {
return DEFAULT_EMBED_MODEL
}
return trimmed
}
function normalizeRerankModel(value) {
return normalizeEmbedModel(value)
}
function normalizeModelName(value, fallback = '') {
const trimmed = String(value || '').trim()
return trimmed || fallback
}
function normalizeTranscriptionModel(value) {
return normalizeModelName(value, DEFAULT_TRANSCRIPTION_MODEL)
}
function looksLikeOllamaUrl(value) {
if (typeof value !== 'string') {
return false
}
try {
const parsed = new URL(value)
if (parsed.port === '11434') {
return true
}
return /^\/api\/?$/i.test(parsed.pathname || '')
} catch (_error) {
return false
}
}
function migrateSettings(rawSettings) {
const source = rawSettings && typeof rawSettings === 'object' ? rawSettings : {}
const nextSettings = { ...defaultSettings, ...source }
let migrated = false
if (!Object.prototype.hasOwnProperty.call(source, 'backendApiUrl') && typeof source.ollamaApiUrl === 'string') {
if (looksLikeOllamaUrl(source.ollamaApiUrl)) {
nextSettings.backendApiUrl = DEFAULT_BACKEND_API_URL
nextSettings.ollamaApiUrl = source.ollamaApiUrl
} else {
nextSettings.backendApiUrl = source.ollamaApiUrl
nextSettings.ollamaApiUrl = DEFAULT_OLLAMA_API_URL
}
migrated = true
}
if (!Object.prototype.hasOwnProperty.call(source, 'openDevToolsOnStartup')) {
migrated = true
}
if (!Object.prototype.hasOwnProperty.call(source, 'rerankModel')) {
nextSettings.rerankModel = nextSettings.embedModel
migrated = true
}
if (!Object.prototype.hasOwnProperty.call(source, 'visionModel')) {
nextSettings.visionModel = nextSettings.chatModel || ''
migrated = true
}
if (!Object.prototype.hasOwnProperty.call(source, 'transcriptionModel')) {
nextSettings.transcriptionModel = DEFAULT_TRANSCRIPTION_MODEL
migrated = true
}
nextSettings.backendApiUrl = String(nextSettings.backendApiUrl || '').trim()
nextSettings.ollamaApiUrl = String(nextSettings.ollamaApiUrl || '').trim()
nextSettings.chatModel = normalizeModelName(nextSettings.chatModel)
nextSettings.visionModel = normalizeModelName(nextSettings.visionModel)
nextSettings.embedModel = normalizeEmbedModel(nextSettings.embedModel)
nextSettings.rerankModel = normalizeRerankModel(nextSettings.rerankModel)
nextSettings.transcriptionModel = normalizeTranscriptionModel(nextSettings.transcriptionModel)
nextSettings.openDevToolsOnStartup = normalizeOpenDevToolsOnStartup(nextSettings.openDevToolsOnStartup)
nextSettings.audioInputEnabled = normalizeBooleanSetting(nextSettings.audioInputEnabled)
nextSettings.audioInputDeviceId = String(nextSettings.audioInputDeviceId || '').trim()
nextSettings.audioInputLanguage = String(nextSettings.audioInputLanguage || '').trim().toLowerCase()
return { nextSettings, migrated }
}
function normalizeUiScale(value) {
const numericValue = Number(value)
if (!Number.isFinite(numericValue)) {
return DEFAULT_UI_SCALE
}
return Math.min(MAX_UI_SCALE, Math.max(MIN_UI_SCALE, Math.round(numericValue * 100) / 100))
}
function normalizeBooleanSetting(value) {
if (typeof value === 'string') {
const trimmed = value.trim().toLowerCase()
if (trimmed === 'true' || trimmed === '1' || trimmed === 'yes' || trimmed === 'on') {
return true
}
if (trimmed === 'false' || trimmed === '0' || trimmed === 'no' || trimmed === 'off' || trimmed === '') {
return false
}
}
return value === true
}
function normalizeOpenDevToolsOnStartup(value) {
return normalizeBooleanSetting(value)
}
function applyUiScaleToWindow(window) {
if (!window || window.isDestroyed()) {
return
}
window.webContents.setZoomFactor(normalizeUiScale(appSettings.uiScale))
}
function applyUiScaleToAllWindows() {
BrowserWindow.getAllWindows().forEach(applyUiScaleToWindow)
}
function shouldAutoOpenDevTools() {
return is.dev && appSettings.openDevToolsOnStartup === true
}
function applyDevToolsPreferenceToWindow(window) {
if (!is.dev || !window || window.isDestroyed()) {
return
}
const webContents = window.webContents
if (!webContents) {
return
}
if (shouldAutoOpenDevTools()) {
if (!webContents.isDevToolsOpened()) {
webContents.openDevTools({ mode: 'detach' })
}
return
}
if (webContents.isDevToolsOpened()) {
webContents.closeDevTools()
}
}
function applyDevToolsPreferenceToAllWindows() {
BrowserWindow.getAllWindows().forEach(applyDevToolsPreferenceToWindow)
}
function loadSettings() {
try {
if (fs.existsSync(settingsFilePath)) {
const data = fs.readFileSync(settingsFilePath, 'utf8')
const { nextSettings, migrated } = migrateSettings(JSON.parse(data))
appSettings = nextSettings
if (migrated) {
saveSettings()
}
} else {
appSettings = { ...defaultSettings }
saveSettings()
}
appSettings.uiScale = normalizeUiScale(appSettings.uiScale)
} catch (error) {
console.error('Failed to load settings:', error)
appSettings = { ...defaultSettings }
}
}
function saveSettings() {
try {
fs.mkdirSync(path.dirname(settingsFilePath), { recursive: true })
fs.writeFileSync(settingsFilePath, JSON.stringify(appSettings, null, 2), 'utf8')
} catch (error) {
console.error('Failed to save settings:', error)
}
}
function setUpdateStatus(status) {
lastUpdateCheckResult = {
state: 'idle',
message: '',
checkedAt: new Date().toISOString(),
localCommit: null,
remoteCommit: null,
branch: null,
restartScheduled: false,
...status,
}
return lastUpdateCheckResult
}
async function runGitCommand(args, options = {}) {
return execFileAsync('git', ['-C', REPO_ROOT, ...args], {
env: GIT_ENV,
maxBuffer: 1024 * 1024,
timeout: options.timeout ?? 15000,
})
}
function normalizeChangelogPage(value) {
const parsed = Number.parseInt(value, 10)
if (!Number.isFinite(parsed) || parsed < 1) {
return 1
}
return parsed
}
function truncateChangelogMessage(message) {
const normalized = String(message || '')
.replace(/\s+/g, ' ')
.trim()
if (!normalized) {
return 'No commit message.'
}
if (normalized.length <= CHANGELOG_MESSAGE_MAX_LENGTH) {
return normalized
}
return `${normalized.slice(0, CHANGELOG_MESSAGE_MAX_LENGTH - 3).trimEnd()}...`
}
function parseChangelogOutput(output) {
return String(output || '')
.split('\u0000\u0000')
.map((record) => record.trim())
.filter(Boolean)
.map((record) => {
const [hash = '', committedAt = '', ...messageParts] = record.split('\u0000')
return {
hash: hash.trim(),
committedAt: committedAt.trim(),
message: truncateChangelogMessage(messageParts.join('\u0000')),
}
})
.filter((entry) => entry.hash && entry.message)
}
async function getChangelogPage(page = 1) {
const normalizedPage = normalizeChangelogPage(page)
const pageSize = CHANGELOG_PAGE_SIZE
const skip = (normalizedPage - 1) * pageSize
const { stdout } = await runGitCommand([
'log',
`--max-count=${pageSize + 1}`,
`--skip=${skip}`,
'--format=%H%x00%cI%x00%B%x00%x00',
], { timeout: 30000 })
const entries = parseChangelogOutput(stdout)
return {
page: normalizedPage,
pageSize,
hasMore: entries.length > pageSize,
entries: entries.slice(0, pageSize),
}
}
function scheduleAppRestart() {
setTimeout(() => {
if (process.env.HEIMGEIST_DEV_WRAPPER === '1') {
app.exit(DEV_WRAPPER_RELAUNCH_CODE)
return
}
app.relaunch()
app.exit(0)
}, 300)
}
function parseGitStatusPaths(statusOutput) {
return statusOutput
.split('\n')
.map((line) => line.trimEnd())
.filter(Boolean)
.map((line) => line.slice(3).trim())
}
function formatChangedPaths(paths) {
if (!Array.isArray(paths) || paths.length === 0) {
return ''
}
const preview = paths.slice(0, 3).join(', ')
return paths.length > 3 ? `${preview}, ...` : preview
}
async function performUpdateCheck(trigger = 'manual') {
if (!fs.existsSync(path.join(REPO_ROOT, '.git'))) {
return setUpdateStatus({
state: 'unavailable',
trigger,
message: 'Update check unavailable: no Git checkout found.',
})
}
setUpdateStatus({
state: 'checking',
trigger,
message: 'Checking remote repository for updates...',
})
try {
const [
{ stdout: localStdout },
{ stdout: branchStdout },
{ stdout: remoteStdout },
{ stdout: worktreeStdout },
] = await Promise.all([
runGitCommand(['rev-parse', 'HEAD']),
runGitCommand(['branch', '--show-current']),
runGitCommand(['ls-remote', UPDATE_REMOTE_URL, `refs/heads/${UPDATE_BRANCH}`]),
runGitCommand(['status', '--porcelain', '--untracked-files=no']),
])
const localCommit = localStdout.trim()
const branch = branchStdout.trim() || null
const remoteCommit = remoteStdout.trim().split(/\s+/)[0] || null
const changedPaths = parseGitStatusPaths(worktreeStdout)
const worktreeDirty = changedPaths.length > 0
if (!localCommit) {
throw new Error('Could not resolve the current local commit hash.')
}
if (!remoteCommit) {
throw new Error(`Could not resolve the remote ${UPDATE_BRANCH} commit hash.`)
}
if (localCommit === remoteCommit) {
return setUpdateStatus({
state: 'up-to-date',
trigger,
branch,
localCommit,
remoteCommit,
message: 'Heimgeist is already up to date.',
})
}
if (branch && branch !== UPDATE_BRANCH) {
return setUpdateStatus({
state: 'skipped',
trigger,
branch,
localCommit,
remoteCommit,
message: `Update skipped: current branch is "${branch}", expected "${UPDATE_BRANCH}".`,
})
}
if (worktreeDirty) {
const changedPathSummary = formatChangedPaths(changedPaths)
return setUpdateStatus({
state: 'skipped',
trigger,
branch: branch || UPDATE_BRANCH,
localCommit,
remoteCommit,
message: changedPathSummary
? `Update skipped: tracked local changes detected in ${changedPathSummary}.`
: 'Update skipped: tracked local changes detected.',
})
}
setUpdateStatus({
state: 'updating',
trigger,
branch: branch || UPDATE_BRANCH,
localCommit,
remoteCommit,
message: 'Update found. Pulling latest changes and restarting Heimgeist...',
})
await runGitCommand(['pull', '--ff-only', UPDATE_REMOTE_URL, UPDATE_BRANCH], { timeout: 120000 })
const { stdout: updatedStdout } = await runGitCommand(['rev-parse', 'HEAD'])
const updatedLocalCommit = updatedStdout.trim()
if (!updatedLocalCommit || updatedLocalCommit === localCommit) {
return setUpdateStatus({
state: 'up-to-date',
trigger,
branch: branch || UPDATE_BRANCH,
localCommit,
remoteCommit,
message: 'No newer remote update was applied. Local checkout already contains the latest pulled state.',
})
}
const result = setUpdateStatus({
state: 'updated',
trigger,
branch: branch || UPDATE_BRANCH,
localCommit: updatedLocalCommit || localCommit,
remoteCommit,
message: 'Update installed. Heimgeist restarts now.',
restartScheduled: true,
})
scheduleAppRestart()
return result
} catch (error) {
console.error('Failed to check for updates:', error)
return setUpdateStatus({
state: 'error',
trigger,
message: `Update check failed: ${error.message || String(error)}`,
})
}
}
function checkForUpdates(trigger = 'manual') {
if (!activeUpdateCheck) {
activeUpdateCheck = performUpdateCheck(trigger).finally(() => {
activeUpdateCheck = null
})
}
return activeUpdateCheck
}
async function createMainWindow() {
console.log('Electron: creating main window')
mainWindow = new BrowserWindow({
width: 1000,
height: 720,
minWidth: 680,
minHeight: 300,
show: false,
webPreferences: {
preload: path.join(__dirname, 'preload.cjs'),
contextIsolation: true,
nodeIntegration: false,
},
})
applyUiScaleToWindow(mainWindow)
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
mainWindow.webContents.on('did-finish-load', () => {
applyUiScaleToWindow(mainWindow)
})
mainWindow.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL) => {
console.error('Main window failed to load:', { errorCode, errorDescription, validatedURL })
})
mainWindow.on('focus', () => {
mainWindow.webContents.send('window-focused')
})
if (is.dev && process.env.VITE_DEV_SERVER_URL) {
console.log(`Electron: loading renderer ${process.env.VITE_DEV_SERVER_URL}`)
await mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL)
applyDevToolsPreferenceToWindow(mainWindow)
} else {
console.log('Electron: loading bundled renderer')
await mainWindow.loadFile(path.join(__dirname, '../dist/index.html'))
}
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url)
return { action: 'deny' }
})
}
async function createSettingsWindow() {
if (settingsWindow) {
settingsWindow.focus()
return
}
settingsWindow = new BrowserWindow({
width: 800,
height: 600,
title: 'Settings',
parent: mainWindow,
modal: true,
show: false,
webPreferences: {
preload: path.join(__dirname, 'preload.cjs'),
contextIsolation: true,
nodeIntegration: false,
},
})
applyUiScaleToWindow(settingsWindow)
settingsWindow.on('ready-to-show', () => {
settingsWindow.show()
})
settingsWindow.webContents.on('did-finish-load', () => {
applyUiScaleToWindow(settingsWindow)
})
settingsWindow.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL) => {
console.error('Settings window failed to load:', { errorCode, errorDescription, validatedURL })
})
settingsWindow.on('closed', () => {
settingsWindow = null
})
if (is.dev && process.env.VITE_DEV_SERVER_URL) {
await settingsWindow.loadURL(`${process.env.VITE_DEV_SERVER_URL}#/settings`)
applyDevToolsPreferenceToWindow(settingsWindow)
} else {
await settingsWindow.loadFile(path.join(__dirname, '../dist/index.html'), { hash: '/settings' })
}
}
app.whenReady().then(async () => {
console.log('Electron: app ready')
loadSettings()
const startupUpdateResult = await checkForUpdates('startup')
if (startupUpdateResult?.restartScheduled) {
return
}
await createMainWindow()
const menuTemplate = [
{
label: 'File',
submenu: [
{
label: 'Settings',
accelerator: 'CmdOrCtrl+,',
click: createSettingsWindow,
},
{ type: 'separator' },
{ role: 'quit' },
],
},
{
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ role: 'delete' },
{ type: 'separator' },
{ role: 'selectAll' },
],
},
{
label: 'View',
submenu: [
{ role: 'reload' },
{ role: 'forcereload' },
{ role: 'toggledevtools' },
{ type: 'separator' },
{ role: 'resetzoom' },
{ role: 'zoomin' },
{ role: 'zoomout' },
{ type: 'separator' },
{ role: 'togglefullscreen' },
],
},
]
const menu = Menu.buildFromTemplate(menuTemplate)
Menu.setApplicationMenu(menu)
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createMainWindow()
})
})
ipcMain.handle('get-settings', () => appSettings)
ipcMain.handle('get-update-status', () => lastUpdateCheckResult)
ipcMain.handle('check-for-updates', () => checkForUpdates('manual'))
ipcMain.handle('get-changelog-page', (event, page) => getChangelogPage(page))
ipcMain.handle('set-setting', (event, key, value) => {
if (key === 'uiScale') {
appSettings[key] = normalizeUiScale(value)
} else if (key === 'embedModel') {
appSettings[key] = normalizeEmbedModel(value)
} else if (key === 'rerankModel') {
appSettings[key] = normalizeRerankModel(value)
} else if (key === 'chatModel' || key === 'visionModel') {
appSettings[key] = normalizeModelName(value)
} else if (key === 'transcriptionModel') {
appSettings[key] = normalizeTranscriptionModel(value)
} else if (key === 'openDevToolsOnStartup') {
appSettings[key] = normalizeOpenDevToolsOnStartup(value)
} else if (key === 'audioInputEnabled') {
appSettings[key] = normalizeBooleanSetting(value)
} else if (key === 'audioInputDeviceId') {
appSettings[key] = String(value || '').trim()
} else if (key === 'audioInputLanguage') {
appSettings[key] = String(value || '').trim().toLowerCase()
} else {
appSettings[key] = value
}
saveSettings()
if (key === 'uiScale') {
applyUiScaleToAllWindows()
} else if (key === 'openDevToolsOnStartup') {
applyDevToolsPreferenceToAllWindows()
}
return true
})
ipcMain.handle('update-settings', (event, settings) => {
appSettings = { ...appSettings, ...settings }
appSettings.uiScale = normalizeUiScale(appSettings.uiScale)
appSettings.chatModel = normalizeModelName(appSettings.chatModel)
appSettings.visionModel = normalizeModelName(appSettings.visionModel)
appSettings.embedModel = normalizeEmbedModel(appSettings.embedModel)
appSettings.rerankModel = normalizeRerankModel(appSettings.rerankModel)
appSettings.transcriptionModel = normalizeTranscriptionModel(appSettings.transcriptionModel)
appSettings.openDevToolsOnStartup = normalizeOpenDevToolsOnStartup(appSettings.openDevToolsOnStartup)
appSettings.audioInputEnabled = normalizeBooleanSetting(appSettings.audioInputEnabled)
appSettings.audioInputDeviceId = String(appSettings.audioInputDeviceId || '').trim()
appSettings.audioInputLanguage = String(appSettings.audioInputLanguage || '').trim().toLowerCase()
saveSettings()
if (Object.prototype.hasOwnProperty.call(settings, 'uiScale')) {
applyUiScaleToAllWindows()
}
if (Object.prototype.hasOwnProperty.call(settings, 'openDevToolsOnStartup')) {
applyDevToolsPreferenceToAllWindows()
}
return true
})
ipcMain.handle('pick-paths', async (event, options = {}) => {
const dialogOptions = {
properties: ['openFile', 'multiSelections'],
}
if (Array.isArray(options?.filters) && options.filters.length > 0) {
dialogOptions.filters = options.filters
}
if (typeof options?.title === 'string' && options.title.trim()) {
dialogOptions.title = options.title.trim()
}
const result = await dialog.showOpenDialog(mainWindow, {
...dialogOptions,
})
return result.canceled ? [] : result.filePaths
})
ipcMain.handle('open-path', async (event, filePath) => {
if (!filePath) return false
const err = await shell.openPath(filePath)
return err === ''
})
ipcMain.on('open-external-link', (event, url) => {
shell.openExternal(url)
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})

View File

@@ -1,21 +0,0 @@
const { contextBridge, ipcRenderer } = require('electron')
// Expose a secure API to the renderer process
contextBridge.exposeInMainWorld('electronAPI', {
getSettings: () => ipcRenderer.invoke('get-settings'),
getUpdateStatus: () => ipcRenderer.invoke('get-update-status'),
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
getChangelogPage: (page) => ipcRenderer.invoke('get-changelog-page', page),
setSetting: (key, value) => ipcRenderer.invoke('set-setting', key, value),
updateSettings: (settings) => ipcRenderer.invoke('update-settings', settings),
pickPaths: (options) => ipcRenderer.invoke('pick-paths', options),
openPath: (filePath) => ipcRenderer.invoke('open-path', filePath),
openExternalLink: (event) => {
event.preventDefault();
const url = event.currentTarget.href;
ipcRenderer.send('open-external-link', url);
},
onWindowFocus: (callback) => ipcRenderer.on('window-focused', callback),
offWindowFocus: (callback) => ipcRenderer.removeListener('window-focused', callback),
})

View File

@@ -2,23 +2,18 @@
"name": "Heimgeist",
"version": "0.1.0",
"private": true,
"main": "electron/main.cjs",
"type": "module",
"scripts": {
"dev": "concurrently -k \"npm:dev:backend\" \"npm:dev:renderer\" \"npm:dev:electron\"",
"dev:backend": "node scripts/run-backend.cjs",
"dev:renderer": "vite --port 5173 --strictPort",
"dev:renderer:tauri": "vite --host 127.0.0.1 --port 5174 --strictPort",
"dev:electron": "node scripts/run-electron-dev.cjs",
"dev": "node scripts/run-tauri-dev.cjs",
"dev:tauri": "node scripts/run-tauri-dev.cjs",
"dev:tauri:full": "node scripts/run-tauri-dev.cjs",
"dev:backend": "node scripts/run-backend.cjs",
"dev:renderer": "vite --host 127.0.0.1 --port 5174 --strictPort",
"dev:tauri:shell": "tauri dev",
"tauri": "tauri",
"build": "vite build",
"start": "electron ."
"start": "npm run dev"
},
"dependencies": {
"@electron-toolkit/utils": "^4.0.0",
"ffmpeg-static": "^5.3.0",
"ffprobe-static": "^3.1.0",
"react": "^18.3.1",
@@ -29,9 +24,6 @@
"devDependencies": {
"@tauri-apps/cli": "^2.11.0",
"@vitejs/plugin-react": "^4.3.1",
"concurrently": "^9.0.1",
"cross-env": "^7.0.3",
"electron": "^31.2.0",
"vite": "^5.4.2",
"wait-on": "^7.2.0"
}

22
run.sh
View File

@@ -64,12 +64,15 @@ PY
node_deps_usable() {
node - <<'NODE' >/dev/null 2>&1
require('concurrently')
require('electron')
require('react')
require('vite')
require('wait-on')
NODE
if [ "$?" -ne 0 ]; then
return 1
fi
[ -x node_modules/.bin/tauri ] || [ -x node_modules/.bin/tauri.cmd ]
}
is_linux_x86_64() {
@@ -169,21 +172,6 @@ fi
TORCH_FLAVOR="$(resolve_torch_flavor)"
RECREATE_VENV=0
if [ -z "${HEIMGEIST_SETTINGS_FILE:-}" ]; then
case "$(uname -s)" in
Darwin)
HEIMGEIST_SETTINGS_FILE="${HOME}/Library/Application Support/Heimgeist/settings.json"
;;
Linux)
HEIMGEIST_SETTINGS_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/Heimgeist/settings.json"
;;
esac
if [ -n "${HEIMGEIST_SETTINGS_FILE:-}" ]; then
export HEIMGEIST_SETTINGS_FILE
mkdir -p "$(dirname "$HEIMGEIST_SETTINGS_FILE")"
fi
fi
if [ ! -x "$VENV_DIR/bin/python" ] || ! "$VENV_DIR/bin/python" -c 'import sys; raise SystemExit(0 if sys.version_info[:2] == (3, 13) else 1)'; then
RECREATE_VENV=1
fi

View File

@@ -1,70 +0,0 @@
const { spawn } = require('child_process')
const path = require('path')
const waitOn = require('wait-on')
const projectRoot = path.resolve(__dirname, '..')
const electronBinary = require('electron')
const relaunchExitCode = 75
const devServerUrl = process.env.VITE_DEV_SERVER_URL || 'http://127.0.0.1:5173'
const startupResources = ['http-get://127.0.0.1:5173', 'http-get://127.0.0.1:8000/health']
const env = {
...process.env,
VITE_DEV_SERVER_URL: devServerUrl,
HEIMGEIST_DEV_WRAPPER: '1',
HEIMGEIST_DEV_RELAUNCH_CODE: String(relaunchExitCode),
}
async function waitForDependencies() {
await waitOn({
resources: startupResources,
proxy: false,
timeout: 120000,
})
}
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
function runElectronOnce() {
return new Promise((resolve) => {
const child = spawn(electronBinary, ['.'], {
cwd: projectRoot,
env,
stdio: 'inherit',
})
child.on('exit', (code, signal) => {
resolve({ code, signal })
})
})
}
async function main() {
await waitForDependencies()
console.log(`Electron dev wrapper: dependencies ready, launching ${env.VITE_DEV_SERVER_URL}`)
while (true) {
const { code, signal } = await runElectronOnce()
if (signal) {
process.kill(process.pid, signal)
return
}
if (code === relaunchExitCode) {
await sleep(750)
await waitForDependencies()
continue
}
process.exit(code ?? 0)
}
}
main().catch((error) => {
console.error('Failed to launch Electron dev wrapper:', error)
process.exit(1)
})

View File

@@ -4,7 +4,7 @@
"version": "0.1.0",
"identifier": "com.giers.heimgeist",
"build": {
"beforeDevCommand": "npm run dev:renderer:tauri",
"beforeDevCommand": "npm run dev:renderer",
"beforeBuildCommand": "npm run build",
"devUrl": "http://127.0.0.1:5174",
"frontendDist": "../dist"