diff --git a/gitnexus-desktop/package-lock.json b/gitnexus-desktop/package-lock.json index b44096184..cd492c405 100644 --- a/gitnexus-desktop/package-lock.json +++ b/gitnexus-desktop/package-lock.json @@ -4934,23 +4934,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react": { - "version": "19.2.5", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.5", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.5" - } - }, "node_modules/react-refresh": { "version": "0.18.0", "dev": true, @@ -5155,10 +5138,6 @@ "node": ">=11.0.0" } }, - "node_modules/scheduler": { - "version": "0.27.0", - "license": "MIT" - }, "node_modules/semver": { "version": "6.3.1", "dev": true, diff --git a/gitnexus-desktop/scripts/ensure-gitnexus-runtime.mjs b/gitnexus-desktop/scripts/ensure-gitnexus-runtime.mjs index 1d0a182ec..a6df9d098 100644 --- a/gitnexus-desktop/scripts/ensure-gitnexus-runtime.mjs +++ b/gitnexus-desktop/scripts/ensure-gitnexus-runtime.mjs @@ -1,5 +1,17 @@ import { spawnSync } from 'node:child_process'; -import { existsSync, readdirSync, statSync } from 'node:fs'; +import { + cpSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + statSync, + writeFileSync, +} from 'node:fs'; +import { createRequire } from 'node:module'; +import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -9,11 +21,56 @@ const workspaceRoot = path.resolve(__dirname, '..', '..'); const sharedRoot = path.join(workspaceRoot, 'gitnexus-shared'); const gitnexusRoot = path.join(workspaceRoot, 'gitnexus'); const gitnexusWebRoot = path.join(workspaceRoot, 'gitnexus-web'); +const gitnexusLockfile = JSON.parse( + readFileSync(path.join(gitnexusRoot, 'package-lock.json'), 'utf8'), +); +const gitnexusPackageJson = JSON.parse( + readFileSync(path.join(gitnexusRoot, 'package.json'), 'utf8'), +); +const gitnexusModuleResolver = createRequire(path.join(gitnexusRoot, 'package.json')); const gitnexusServerEntry = path.join(gitnexusRoot, 'dist', 'server', 'api.js'); const gitnexusCliEntry = path.join(gitnexusRoot, 'dist', 'cli', 'index.js'); +const gitnexusServerPort = 4747; +const gitnexusWebDevPort = 5173; const desktopRendererPort = 5174; const shouldCleanupDevPort = process.argv.includes('--cleanup-dev-port'); +const getNodeModulePackagePath = (packageRoot, packageName) => { + return path.join(packageRoot, 'node_modules', ...packageName.split('/')); +}; + +const getNodeModulePackageJsonPath = (packageRoot, packageName) => { + return path.join(packageRoot, 'node_modules', ...packageName.split('/'), 'package.json'); +}; + +const getLockedPackageInstallSpecifier = (packageName) => { + const lockedPackage = gitnexusLockfile.packages?.[`node_modules/${packageName}`]; + const lockedVersion = lockedPackage?.version; + + if (!lockedVersion || String(lockedVersion).startsWith('file:')) { + return null; + } + + return lockedVersion; +}; + +const getDependencySubset = (dependencyMap, packageNames, overrides = {}) => { + return Object.fromEntries( + packageNames.map((packageName) => { + const versionSpecifier = + overrides[packageName] ?? + getLockedPackageInstallSpecifier(packageName) ?? + dependencyMap?.[packageName]; + + if (!versionSpecifier) { + throw new Error(`Missing package specifier for ${packageName}.`); + } + + return [packageName, versionSpecifier]; + }), + ); +}; + const getEntryMtimeMs = (targetPath) => { if (!existsSync(targetPath)) { return 0; @@ -90,11 +147,58 @@ const gitnexusInstallInputs = [ path.join(sharedRoot, 'package-lock.json'), ]; -const gitnexusInstallMarkerPaths = [ - path.join(gitnexusRoot, 'node_modules', 'tsx'), - path.join(gitnexusRoot, 'node_modules', 'typescript'), +const gitnexusDesktopRuntimeDependencyNames = [ + '@ladybugdb/core', + '@modelcontextprotocol/sdk', + 'cli-progress', + 'commander', + 'cors', + 'express', + 'glob', + 'graphology', + 'graphology-indices', + 'graphology-utils', + 'ignore', + 'js-yaml', + 'lru-cache', + 'mnemonist', + 'pandemonium', + 'uuid', ]; +const gitnexusDesktopBuildDependencyNames = ['@types/node', 'gitnexus-shared', 'typescript']; + +const gitnexusDesktopRuntimeDependencies = getDependencySubset( + gitnexusPackageJson.dependencies ?? {}, + gitnexusDesktopRuntimeDependencyNames, +); + +const gitnexusDesktopBuildDependencies = getDependencySubset( + gitnexusPackageJson.devDependencies ?? {}, + gitnexusDesktopBuildDependencyNames, + { 'gitnexus-shared': `file:${sharedRoot}` }, +); + +const gitnexusDesktopRuntimeInstallMarkerPaths = [ + ...gitnexusDesktopRuntimeDependencyNames.map((packageName) => + getNodeModulePackageJsonPath(gitnexusRoot, packageName), + ), + path.join(gitnexusRoot, 'node_modules', 'commander', 'index.js'), + path.join(gitnexusRoot, 'node_modules', '@ladybugdb', 'core', 'lbugjs.node'), +]; + +const gitnexusDesktopRuntimeResolutionChecks = [ + 'commander', + '@modelcontextprotocol/sdk/server/index.js', + '@modelcontextprotocol/sdk/server/streamableHttp.js', + 'ignore', + 'js-yaml', +]; + +const gitnexusDesktopBuildInstallMarkerPaths = gitnexusDesktopBuildDependencyNames.map( + (packageName) => getNodeModulePackageJsonPath(gitnexusRoot, packageName), +); + const gitnexusRuntimeInputs = [ path.join(gitnexusRoot, 'src'), path.join(gitnexusRoot, 'scripts'), @@ -116,14 +220,32 @@ const isInstallStale = (inputPaths, installMarkerPaths) => { return getNewestMtimeMs(inputPaths) > installMarkerMtimeMs; }; -const isGitNexusInstallStale = () => { - return isInstallStale(gitnexusInstallInputs, gitnexusInstallMarkerPaths); -}; - const isGitNexusSharedInstallStale = () => { return isInstallStale(gitnexusSharedInstallInputs, gitnexusSharedInstallMarkerPaths); }; +const canResolveGitNexusSpecifier = (specifier) => { + try { + gitnexusModuleResolver.resolve(specifier); + return true; + } catch { + return false; + } +}; + +const isGitNexusRuntimeInstallStale = () => { + return ( + isInstallStale(gitnexusInstallInputs, gitnexusDesktopRuntimeInstallMarkerPaths) || + gitnexusDesktopRuntimeResolutionChecks.some( + (specifier) => !canResolveGitNexusSpecifier(specifier), + ) + ); +}; + +const isGitNexusBuildInstallStale = () => { + return isInstallStale(gitnexusInstallInputs, gitnexusDesktopBuildInstallMarkerPaths); +}; + const isGitNexusBuildStale = () => { return getNewestMtimeMs(gitnexusRuntimeInputs) > getOldestOutputMtimeMs(gitnexusRuntimeOutputs); }; @@ -150,6 +272,56 @@ const runNpm = (args, cwd) => { } }; +const overlayDirectoryContents = (sourceDirectory, destinationDirectory) => { + mkdirSync(destinationDirectory, { recursive: true }); + + for (const entry of readdirSync(sourceDirectory, { withFileTypes: true })) { + cpSync(path.join(sourceDirectory, entry.name), path.join(destinationDirectory, entry.name), { + force: true, + recursive: true, + }); + } +}; + +const replaceDirectGitNexusPackages = (dependencyMap) => { + for (const packageName of Object.keys(dependencyMap)) { + rmSync(getNodeModulePackagePath(gitnexusRoot, packageName), { + force: true, + recursive: true, + }); + } +}; + +const repairGitNexusPackages = (dependencyMap, label) => { + const repairDirectory = mkdtempSync(path.join(os.tmpdir(), 'gitnexus-desktop-gitnexus-repair-')); + + try { + writeFileSync( + path.join(repairDirectory, 'package.json'), + JSON.stringify( + { + name: 'gitnexus-desktop-gitnexus-repair', + private: true, + dependencies: dependencyMap, + }, + null, + 2, + ), + ); + + console.info(`[gitnexus-desktop] ${label}.`); + runNpm(['install', '--no-package-lock'], repairDirectory); + + replaceDirectGitNexusPackages(dependencyMap); + overlayDirectoryContents( + path.join(repairDirectory, 'node_modules'), + path.join(gitnexusRoot, 'node_modules'), + ); + } finally { + rmSync(repairDirectory, { force: true, recursive: true }); + } +}; + const runCommand = (command, args) => { return spawnSync(command, args, { encoding: 'utf8', @@ -250,8 +422,56 @@ const ensureDesktopRendererPortAvailable = () => { killProcessTree(owner.pid); }; +const ensureGitNexusWebDevPortAvailable = () => { + const owner = findPortOwner(gitnexusWebDevPort); + + if (!owner) { + return; + } + + const signature = `${owner.name ?? ''} ${owner.commandLine ?? ''}`.toLowerCase(); + const isGitNexusWebViteProcess = signature.includes('vite') && signature.includes('gitnexus-web'); + + if (!isGitNexusWebViteProcess) { + console.error( + `Port ${gitnexusWebDevPort} is already in use by ${owner.name ?? 'another process'} (PID ${owner.pid}). Stop that process and try again.`, + ); + process.exit(1); + } + + console.info( + `Stopping stale GitNexus web dev server on port ${gitnexusWebDevPort} (PID ${owner.pid}).`, + ); + killProcessTree(owner.pid); +}; + +const ensureGitNexusServerPortAvailable = () => { + const owner = findPortOwner(gitnexusServerPort); + + if (!owner) { + return; + } + + const signature = `${owner.name ?? ''} ${owner.commandLine ?? ''}`.toLowerCase(); + const isGitNexusServerProcess = + (signature.includes('dist\\cli\\index.js') || signature.includes('dist/cli/index.js')) && + signature.includes('serve'); + + if (!isGitNexusServerProcess) { + console.error( + `Port ${gitnexusServerPort} is already in use by ${owner.name ?? 'another process'} (PID ${owner.pid}). Stop that process and try again.`, + ); + process.exit(1); + } + + console.info(`Stopping stale GitNexus backend on port ${gitnexusServerPort} (PID ${owner.pid}).`); + killProcessTree(owner.pid); +}; + if (shouldCleanupDevPort) { + ensureGitNexusServerPortAvailable(); ensureDesktopRendererPortAvailable(); + ensureGitNexusWebDevPortAvailable(); } if ( @@ -262,9 +482,8 @@ if ( runNpm(['ci'], sharedRoot); } -if (!existsSync(path.join(gitnexusRoot, 'node_modules', 'tsx')) || isGitNexusInstallStale()) { - console.info('[gitnexus-desktop] Refreshing GitNexus dependencies.'); - runNpm(['ci'], gitnexusRoot); +if (isGitNexusRuntimeInstallStale()) { + repairGitNexusPackages(gitnexusDesktopRuntimeDependencies, 'Repairing GitNexus runtime packages'); } if (!existsSync(path.join(gitnexusWebRoot, 'node_modules', 'vite'))) { @@ -272,6 +491,16 @@ if (!existsSync(path.join(gitnexusWebRoot, 'node_modules', 'vite'))) { } if (getOldestOutputMtimeMs(gitnexusRuntimeOutputs) === 0 || isGitNexusBuildStale()) { + if (isGitNexusBuildInstallStale()) { + repairGitNexusPackages( + { + ...gitnexusDesktopRuntimeDependencies, + ...gitnexusDesktopBuildDependencies, + }, + 'Repairing GitNexus build tooling', + ); + } + console.info('[gitnexus-desktop] Rebuilding GitNexus runtime artifacts.'); runNpm(['run', 'build'], gitnexusRoot); } diff --git a/gitnexus-desktop/scripts/package.mjs b/gitnexus-desktop/scripts/package.mjs index da5910d84..5637f00d8 100644 --- a/gitnexus-desktop/scripts/package.mjs +++ b/gitnexus-desktop/scripts/package.mjs @@ -12,6 +12,12 @@ const workspaceRoot = path.resolve(packageRoot, '..'); const gitnexusRoot = path.join(workspaceRoot, 'gitnexus'); const gitnexusWebRoot = path.join(workspaceRoot, 'gitnexus-web'); const releaseRoot = path.join(packageRoot, 'release'); +const gitnexusPackageJson = JSON.parse( + fs.readFileSync(path.join(gitnexusRoot, 'package.json'), 'utf8'), +); +const gitnexusPackageLock = JSON.parse( + fs.readFileSync(path.join(gitnexusRoot, 'package-lock.json'), 'utf8'), +); const desktopPackageJson = JSON.parse( fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'), ); @@ -52,20 +58,90 @@ const supportedTargetHosts = { '--win': 'win32', }; const artifactFileExtensions = new Set(['.AppImage', '.dmg', '.exe', '.msi', '.zip']); +const gitnexusRuntimeDependencyNames = Object.keys(gitnexusPackageJson.dependencies ?? {}); +const packagedResourceEntries = [ + { + from: path.join(gitnexusWebRoot, 'dist'), + to: 'gitnexus-web', + }, + { + from: path.join(gitnexusRoot, 'dist'), + to: path.join('gitnexus', 'dist'), + }, + { + from: path.join(gitnexusRoot, 'hooks'), + to: path.join('gitnexus', 'hooks'), + }, + { + from: path.join(gitnexusRoot, 'skills'), + to: path.join('gitnexus', 'skills'), + }, + { + from: path.join(gitnexusRoot, 'vendor'), + to: path.join('gitnexus', 'vendor'), + }, + { + from: path.join(gitnexusRoot, 'package.json'), + to: path.join('gitnexus', 'package.json'), + }, +]; + +const toBuilderRelativePath = (targetPath) => { + return path.relative(packageRoot, targetPath) || '.'; +}; + +const getNodeModulePath = (packageName) => { + return path.join(gitnexusRoot, 'node_modules', ...packageName.split('/')); +}; + +const getRuntimeDependencyClosure = () => { + const visited = new Set(); + const queue = [...gitnexusRuntimeDependencyNames]; + + while (queue.length > 0) { + const packageName = queue.shift(); + + if (!packageName || visited.has(packageName)) { + continue; + } + + visited.add(packageName); + + const lockEntry = gitnexusPackageLock.packages?.[`node_modules/${packageName}`]; + const dependencyNames = Object.keys({ + ...(lockEntry?.dependencies ?? {}), + ...(lockEntry?.optionalDependencies ?? {}), + }); + + for (const dependencyName of dependencyNames) { + if (!visited.has(dependencyName)) { + queue.push(dependencyName); + } + } + } + + return [...visited].filter((packageName) => fs.existsSync(getNodeModulePath(packageName))).sort(); +}; + +const packagedRuntimeNodeModules = getRuntimeDependencyClosure(); const builderArgs = process.argv.slice(2); const requestedTargets = builderArgs.filter((argument) => argument in supportedTargetHosts); const builderEnvironment = { - GITNEXUS_DESKTOP_GITNEXUS_DIST: path.join(gitnexusRoot, 'dist'), - GITNEXUS_DESKTOP_GITNEXUS_HOOKS: path.join(gitnexusRoot, 'hooks'), - GITNEXUS_DESKTOP_GITNEXUS_NODE_MODULES: path.join(gitnexusRoot, 'node_modules'), - GITNEXUS_DESKTOP_GITNEXUS_PACKAGE_JSON: path.join(gitnexusRoot, 'package.json'), - GITNEXUS_DESKTOP_GITNEXUS_SKILLS: path.join(gitnexusRoot, 'skills'), - GITNEXUS_DESKTOP_GITNEXUS_VENDOR: path.join(gitnexusRoot, 'vendor'), + GITNEXUS_DESKTOP_GITNEXUS_DIST: toBuilderRelativePath(path.join(gitnexusRoot, 'dist')), + GITNEXUS_DESKTOP_GITNEXUS_HOOKS: toBuilderRelativePath(path.join(gitnexusRoot, 'hooks')), + GITNEXUS_DESKTOP_GITNEXUS_NODE_MODULES: toBuilderRelativePath( + path.join(gitnexusRoot, 'node_modules'), + ), + GITNEXUS_DESKTOP_GITNEXUS_PACKAGE_JSON: toBuilderRelativePath( + path.join(gitnexusRoot, 'package.json'), + ), + GITNEXUS_DESKTOP_GITNEXUS_SKILLS: toBuilderRelativePath(path.join(gitnexusRoot, 'skills')), + GITNEXUS_DESKTOP_GITNEXUS_VENDOR: toBuilderRelativePath(path.join(gitnexusRoot, 'vendor')), GITNEXUS_DESKTOP_NODE_EXECUTABLE: process.execPath, GITNEXUS_DESKTOP_NODE_RESOURCE_PATH: `gitnexus-node/${bundledNodeExecutableName}`, - GITNEXUS_DESKTOP_WEB_DIST: path.join(gitnexusWebRoot, 'dist'), + GITNEXUS_DESKTOP_WEB_DIST: toBuilderRelativePath(path.join(gitnexusWebRoot, 'dist')), }; const builderCommand = [ @@ -97,6 +173,33 @@ const tryRunCommand = (command, cwd, extraEnv = {}) => { } }; +const mirrorDirectory = (sourceDirectory, destinationDirectory) => { + if (process.platform === 'win32') { + fs.mkdirSync(destinationDirectory, { recursive: true }); + + try { + execSync( + `robocopy "${sourceDirectory}" "${destinationDirectory}" /MIR /NFL /NDL /NJH /NJS /NP`, + { + cwd: packageRoot, + stdio: 'inherit', + }, + ); + } catch (error) { + const exitCode = error.status ?? 16; + + if (exitCode > 7) { + throw error; + } + } + + return; + } + + fs.rmSync(destinationDirectory, { force: true, recursive: true }); + fs.cpSync(sourceDirectory, destinationDirectory, { force: true, recursive: true }); +}; + const canResolveBuilderRuntimeModule = (moduleName) => { try { builderUtilRequire.resolve(moduleName); @@ -117,6 +220,63 @@ const overlayDirectoryContents = (sourceDirectory, destinationDirectory) => { } }; +const resolvePackagedResourceRoots = () => { + const candidateRoots = [ + path.join(outputDir, 'win-unpacked', 'resources'), + path.join(outputDir, 'linux-unpacked', 'resources'), + path.join( + outputDir, + `${desktopPackageJson.productName ?? 'GitNexus Desktop'}.app`, + 'Contents', + 'Resources', + ), + ]; + + return candidateRoots.filter((candidateRoot) => fs.existsSync(candidateRoot)); +}; + +const syncPackagedRuntimeResources = () => { + const resourceRoots = resolvePackagedResourceRoots(); + + if (resourceRoots.length === 0) { + return; + } + + for (const resourceRoot of resourceRoots) { + const packagedNodeModulesRoot = path.join(resourceRoot, 'gitnexus', 'node_modules'); + + fs.rmSync(packagedNodeModulesRoot, { force: true, recursive: true }); + fs.mkdirSync(packagedNodeModulesRoot, { recursive: true }); + + const sourceNodeModulesLock = path.join(gitnexusRoot, 'node_modules', '.package-lock.json'); + const destinationNodeModulesLock = path.join(packagedNodeModulesRoot, '.package-lock.json'); + + if (fs.existsSync(sourceNodeModulesLock)) { + fs.cpSync(sourceNodeModulesLock, destinationNodeModulesLock, { force: true }); + } + + for (const packageName of packagedRuntimeNodeModules) { + const sourcePackagePath = getNodeModulePath(packageName); + const destinationPackagePath = path.join(packagedNodeModulesRoot, ...packageName.split('/')); + + mirrorDirectory(sourcePackagePath, destinationPackagePath); + } + + for (const entry of packagedResourceEntries) { + const destinationPath = path.join(resourceRoot, entry.to); + + if (fs.statSync(entry.from).isDirectory()) { + mirrorDirectory(entry.from, destinationPath); + continue; + } + + fs.rmSync(destinationPath, { force: true, recursive: true }); + fs.mkdirSync(path.dirname(destinationPath), { recursive: true }); + fs.cpSync(entry.from, destinationPath, { force: true }); + } + } +}; + const repairAppBuilderLibPackage = () => { const repairDirectory = fs.mkdtempSync( path.join(os.tmpdir(), 'gitnexus-desktop-app-builder-lib-'), @@ -262,6 +422,7 @@ runCommand('npm run bundle', packageRoot); runCommand('npm run build', gitnexusWebRoot); runCommand(builderCommand, packageRoot, builderEnvironment); +syncPackagedRuntimeResources(); const artifacts = listArtifacts(outputDir); diff --git a/gitnexus-desktop/src/main/main.ts b/gitnexus-desktop/src/main/main.ts index a226c0364..4103be9d4 100644 --- a/gitnexus-desktop/src/main/main.ts +++ b/gitnexus-desktop/src/main/main.ts @@ -33,7 +33,8 @@ const GITNEXUS_WEB_DEV_ROOT_FALLBACK = path.resolve(__dirname, '../../../gitnexu const GITNEXUS_PACKAGED_RUNTIME_DIR = path.join(process.resourcesPath, 'gitnexus'); const GITNEXUS_WEB_PACKAGED_DIR = path.join(process.resourcesPath, 'gitnexus-web'); const GITNEXUS_WEB_EXPECTED_MARKERS = ['GitNexus', '
']; -const GITNEXUS_WEB_READY_TIMEOUT_MS = 30_000; +const GITNEXUS_SERVER_READY_TIMEOUT_MS = 30_000; +const GITNEXUS_WEB_READY_TIMEOUT_MS = 60_000; const GITNEXUS_WEB_READY_POLL_MS = 500; const PACKAGED_WEB_SERVER_HOST = '127.0.0.1'; @@ -67,6 +68,10 @@ let packagedWebServerUrl: string | null = null; const openWindows = new Set(); const contentViews = new WeakMap(); +const getDesktopShellTitlebarHeight = (): number => { + return process.platform === 'darwin' ? DESKTOP_SHELL_TITLEBAR_HEIGHT : 0; +}; + type DesktopShellState = { appName: string; isAlwaysOnTop: boolean; @@ -222,7 +227,15 @@ const getGitNexusWebDevRoot = (): string => { return existsSync(GITNEXUS_WEB_DEV_ROOT) ? GITNEXUS_WEB_DEV_ROOT : GITNEXUS_WEB_DEV_ROOT_FALLBACK; }; +const getGitNexusWebViteCliEntry = (): string => { + return path.join(getGitNexusWebDevRoot(), 'node_modules', 'vite', 'bin', 'vite.js'); +}; + const getNodeCommand = (): string => { + if (app.isPackaged) { + return process.execPath; + } + return ( process.env.npm_node_execpath || process.env.NODE || @@ -230,15 +243,26 @@ const getNodeCommand = (): string => { ); }; +const getNodeProcessEnvironment = (overrides: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv => { + const environment = { + ...process.env, + ...overrides, + }; + + if (app.isPackaged) { + environment.ELECTRON_RUN_AS_NODE = '1'; + } + + return environment; +}; + const spawnGitNexusServer = (): ChildProcess => { const childProcess = spawn( getNodeCommand(), [getGitNexusCliEntry(), 'serve', '--host', GITNEXUS_HOST], { cwd: getGitNexusRuntimeDir(), - env: { - ...process.env, - }, + env: getNodeProcessEnvironment(), stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, }, @@ -262,7 +286,7 @@ const spawnGitNexusServer = (): ChildProcess => { }; const waitForGitNexusServerReady = async (): Promise => { - const deadline = Date.now() + GITNEXUS_WEB_READY_TIMEOUT_MS; + const deadline = Date.now() + GITNEXUS_SERVER_READY_TIMEOUT_MS; while (Date.now() < deadline) { if (await isAnyHttpUrlReady(GITNEXUS_SERVER_HEALTH_URLS)) { @@ -290,35 +314,23 @@ const waitForGitNexusServerReady = async (): Promise => { }; const spawnWebDevServer = (): ChildProcess => { - const command = process.platform === 'win32' ? 'cmd.exe' : 'npm'; - const args = - process.platform === 'win32' - ? [ - '/d', - '/s', - '/c', - `npm run dev -- --host ${GITNEXUS_WEB_DEV_HOST} --port ${GITNEXUS_WEB_DEV_PORT} --strictPort`, - ] - : [ - 'run', - 'dev', - '--', - '--host', - GITNEXUS_WEB_DEV_HOST, - '--port', - String(GITNEXUS_WEB_DEV_PORT), - '--strictPort', - ]; - - const childProcess = spawn(command, args, { - cwd: getGitNexusWebDevRoot(), - env: { - ...process.env, - BROWSER: 'none', + const childProcess = spawn( + getNodeCommand(), + [ + getGitNexusWebViteCliEntry(), + '--host', + GITNEXUS_WEB_DEV_HOST, + '--port', + String(GITNEXUS_WEB_DEV_PORT), + '--strictPort', + ], + { + cwd: getGitNexusWebDevRoot(), + env: getNodeProcessEnvironment({ BROWSER: 'none' }), + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true, }, - stdio: ['ignore', 'pipe', 'pipe'], - windowsHide: true, - }); + ); childProcess.stdout?.on('data', (chunk) => { appendProcessOutput(chunk); @@ -489,7 +501,7 @@ const getDesktopShellState = (window: BrowserWindow): DesktopShellState => { isAlwaysOnTop: window.isAlwaysOnTop(), isMaximized: window.isMaximized() || window.isFullScreen(), platform: process.platform, - titleBarHeight: DESKTOP_SHELL_TITLEBAR_HEIGHT, + titleBarHeight: getDesktopShellTitlebarHeight(), }; }; @@ -519,12 +531,13 @@ const updateContentViewBounds = (window: BrowserWindow): void => { } const [width, height] = window.getContentSize(); + const titlebarHeight = getDesktopShellTitlebarHeight(); contentView.setBounds({ x: 0, - y: DESKTOP_SHELL_TITLEBAR_HEIGHT, + y: titlebarHeight, width: Math.max(width, 1), - height: Math.max(height - DESKTOP_SHELL_TITLEBAR_HEIGHT, 1), + height: Math.max(height - titlebarHeight, 1), }); }; @@ -666,9 +679,9 @@ async function createWindow(): Promise { title: DESKTOP_APP_NAME, icon: DESKTOP_APP_ICON_PATH, backgroundColor: DESKTOP_BACKGROUND_COLOR, - frame: false, show: false, autoHideMenuBar: true, + titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default', webPreferences: { preload: path.join(__dirname, '../preload/preload.js'), contextIsolation: true, diff --git a/gitnexus-web/src/index.css b/gitnexus-web/src/index.css index 6c660b0a7..1d62f9215 100644 --- a/gitnexus-web/src/index.css +++ b/gitnexus-web/src/index.css @@ -134,6 +134,8 @@ html, body, #root { height: 100%; + background-color: var(--color-void); + color-scheme: dark; } body { @@ -147,27 +149,32 @@ body { /* ═══════════════════════════════════════════════════════════════ CUSTOM SCROLLBAR ═══════════════════════════════════════════════════════════════ */ -.scrollbar-thin { - scrollbar-width: thin; - scrollbar-color: #2a2a3a #0a0a10; +*::-webkit-scrollbar { + width: 10px; + height: 10px; } -.scrollbar-thin::-webkit-scrollbar { - width: 8px; - height: 8px; +*::-webkit-scrollbar-track { + background: #07070d; } -.scrollbar-thin::-webkit-scrollbar-track { - background: var(--color-deep); +*::-webkit-scrollbar-thumb { + min-height: 44px; + background: #3a3d4f; + border: 2px solid #07070d; + border-radius: 999px; } -.scrollbar-thin::-webkit-scrollbar-thumb { - background: var(--color-border-default); - border-radius: 4px; +*::-webkit-scrollbar-thumb:hover { + background: #50546b; } -.scrollbar-thin::-webkit-scrollbar-thumb:hover { - background: var(--color-text-muted); +*::-webkit-scrollbar-thumb:active { + background: #656b88; +} + +*::-webkit-scrollbar-corner { + background: #07070d; } /* ═══════════════════════════════════════════════════════════════