diff --git a/.github/workflows/desktop-packaging.yml b/.github/workflows/desktop-packaging.yml index dc2279a6c..2b7d89787 100644 --- a/.github/workflows/desktop-packaging.yml +++ b/.github/workflows/desktop-packaging.yml @@ -7,12 +7,11 @@ on: - gitnexus-desktop/** workflow_dispatch: -permissions: - contents: read +permissions: {} concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: false jobs: package: diff --git a/gitnexus-desktop/README.md b/gitnexus-desktop/README.md index 207b77a06..e1a39f258 100644 --- a/gitnexus-desktop/README.md +++ b/gitnexus-desktop/README.md @@ -17,15 +17,16 @@ npm run build:linux - `npm run build:win` creates a Windows NSIS installer `.exe`. - `npm run build:mac` creates a macOS `.dmg`. - `npm run build:linux` creates a Linux `.AppImage`. +- `npm run smoke:unpacked` launches the latest unpacked desktop build, verifies startup, and exits. Build output is written under `gitnexus-desktop/release//`. ## PR Artifacts -The `Desktop Packaging` GitHub Actions workflow uploads preview desktop artifacts for pull requests that touch `gitnexus-desktop/**` or `.github/workflows/desktop-packaging.yml`. +The `Desktop Packaging` GitHub Actions workflow builds an unpacked desktop app, smoke-tests startup, then uploads preview desktop artifacts for pull requests that touch `.github/workflows/desktop-packaging.yml`, `gitnexus-desktop/**`, `gitnexus/**`, `gitnexus-web/**`, or `gitnexus-shared/**`. - `gitnexus-desktop-windows` contains the Windows NSIS installer `.exe` and `win-unpacked/`. - `gitnexus-desktop-macos` contains the macOS `.dmg` and `mac*/` output. - `gitnexus-desktop-linux` contains the Linux `.AppImage` and `linux-unpacked/`. -GitHub artifact URLs are run-specific and expire, so use the artifact names above from the latest successful PR workflow run. \ No newline at end of file +GitHub artifact URLs are run-specific and expire, so use the artifact names above from the latest successful PR workflow run. diff --git a/gitnexus-desktop/scripts/ensure-gitnexus-runtime.mjs b/gitnexus-desktop/scripts/ensure-gitnexus-runtime.mjs index 4250fbb01..1d0a182ec 100644 --- a/gitnexus-desktop/scripts/ensure-gitnexus-runtime.mjs +++ b/gitnexus-desktop/scripts/ensure-gitnexus-runtime.mjs @@ -1,5 +1,5 @@ import { spawnSync } from 'node:child_process'; -import { existsSync } from 'node:fs'; +import { existsSync, readdirSync, statSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -10,9 +10,124 @@ const sharedRoot = path.join(workspaceRoot, 'gitnexus-shared'); const gitnexusRoot = path.join(workspaceRoot, 'gitnexus'); const gitnexusWebRoot = path.join(workspaceRoot, 'gitnexus-web'); const gitnexusServerEntry = path.join(gitnexusRoot, 'dist', 'server', 'api.js'); +const gitnexusCliEntry = path.join(gitnexusRoot, 'dist', 'cli', 'index.js'); const desktopRendererPort = 5174; const shouldCleanupDevPort = process.argv.includes('--cleanup-dev-port'); +const getEntryMtimeMs = (targetPath) => { + if (!existsSync(targetPath)) { + return 0; + } + + return statSync(targetPath).mtimeMs; +}; + +const getPathMtimeMs = (targetPath) => { + if (!existsSync(targetPath)) { + return 0; + } + + const stats = statSync(targetPath); + + if (!stats.isDirectory()) { + return stats.mtimeMs; + } + + let newestMtimeMs = stats.mtimeMs; + + for (const entry of readdirSync(targetPath, { withFileTypes: true })) { + if (entry.name === '.git' || entry.name === 'dist' || entry.name === 'node_modules') { + continue; + } + + newestMtimeMs = Math.max(newestMtimeMs, getPathMtimeMs(path.join(targetPath, entry.name))); + } + + return newestMtimeMs; +}; + +const getNewestMtimeMs = (paths) => { + return paths.reduce((latestMtimeMs, targetPath) => { + return Math.max(latestMtimeMs, getPathMtimeMs(targetPath)); + }, 0); +}; + +const getOldestOutputMtimeMs = (paths) => { + let oldestMtimeMs = Number.POSITIVE_INFINITY; + + for (const targetPath of paths) { + const mtimeMs = getPathMtimeMs(targetPath); + + if (mtimeMs === 0) { + return 0; + } + + oldestMtimeMs = Math.min(oldestMtimeMs, mtimeMs); + } + + return Number.isFinite(oldestMtimeMs) ? oldestMtimeMs : 0; +}; + +const gitnexusSharedSourceInputs = [ + path.join(sharedRoot, 'src'), + path.join(sharedRoot, 'package.json'), + path.join(sharedRoot, 'package-lock.json'), + path.join(sharedRoot, 'tsconfig.json'), +]; + +const gitnexusSharedInstallInputs = [ + path.join(sharedRoot, 'package.json'), + path.join(sharedRoot, 'package-lock.json'), + path.join(sharedRoot, 'tsconfig.json'), +]; + +const gitnexusSharedInstallMarkerPaths = [path.join(sharedRoot, 'node_modules', 'typescript')]; + +const gitnexusInstallInputs = [ + path.join(gitnexusRoot, 'package.json'), + path.join(gitnexusRoot, 'package-lock.json'), + path.join(sharedRoot, 'package.json'), + path.join(sharedRoot, 'package-lock.json'), +]; + +const gitnexusInstallMarkerPaths = [ + path.join(gitnexusRoot, 'node_modules', 'tsx'), + path.join(gitnexusRoot, 'node_modules', 'typescript'), +]; + +const gitnexusRuntimeInputs = [ + path.join(gitnexusRoot, 'src'), + path.join(gitnexusRoot, 'scripts'), + path.join(gitnexusRoot, 'package.json'), + path.join(gitnexusRoot, 'package-lock.json'), + path.join(gitnexusRoot, 'tsconfig.json'), + ...gitnexusSharedSourceInputs, +]; + +const gitnexusRuntimeOutputs = [gitnexusServerEntry, gitnexusCliEntry]; + +const isInstallStale = (inputPaths, installMarkerPaths) => { + if (installMarkerPaths.some((targetPath) => !existsSync(targetPath))) { + return true; + } + + const installMarkerMtimeMs = Math.min(...installMarkerPaths.map(getEntryMtimeMs)); + + return getNewestMtimeMs(inputPaths) > installMarkerMtimeMs; +}; + +const isGitNexusInstallStale = () => { + return isInstallStale(gitnexusInstallInputs, gitnexusInstallMarkerPaths); +}; + +const isGitNexusSharedInstallStale = () => { + return isInstallStale(gitnexusSharedInstallInputs, gitnexusSharedInstallMarkerPaths); +}; + +const isGitNexusBuildStale = () => { + return getNewestMtimeMs(gitnexusRuntimeInputs) > getOldestOutputMtimeMs(gitnexusRuntimeOutputs); +}; + const runNpmAttempt = (args, cwd) => { const result = spawnSync('npm', args, { cwd, @@ -139,11 +254,16 @@ if (shouldCleanupDevPort) { ensureDesktopRendererPortAvailable(); } -if (!existsSync(path.join(sharedRoot, 'node_modules', 'typescript'))) { +if ( + !existsSync(path.join(sharedRoot, 'node_modules', 'typescript')) || + isGitNexusSharedInstallStale() +) { + console.info('[gitnexus-desktop] Refreshing gitnexus-shared dependencies.'); runNpm(['ci'], sharedRoot); } -if (!existsSync(path.join(gitnexusRoot, 'node_modules', 'tsx'))) { +if (!existsSync(path.join(gitnexusRoot, 'node_modules', 'tsx')) || isGitNexusInstallStale()) { + console.info('[gitnexus-desktop] Refreshing GitNexus dependencies.'); runNpm(['ci'], gitnexusRoot); } @@ -151,6 +271,7 @@ if (!existsSync(path.join(gitnexusWebRoot, 'node_modules', 'vite'))) { runNpm(['ci'], gitnexusWebRoot); } -if (!existsSync(gitnexusServerEntry)) { +if (getOldestOutputMtimeMs(gitnexusRuntimeOutputs) === 0 || isGitNexusBuildStale()) { + 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 e7e741d9b..da5910d84 100644 --- a/gitnexus-desktop/scripts/package.mjs +++ b/gitnexus-desktop/scripts/package.mjs @@ -1,5 +1,6 @@ import { execSync } from 'node:child_process'; import fs from 'node:fs'; +import { createRequire } from 'node:module'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -21,12 +22,19 @@ const electronVersion = desktopPackageJson.devDependencies?.electron?.replace(/^[^\d]*/, '') ?? '41.2.1'; const electronBuilderVersion = desktopPackageJson.devDependencies?.['electron-builder']?.replace(/^[^\d]*/, '') ?? '26.8.1'; +const bundledNodeExecutableName = process.platform === 'win32' ? 'node.exe' : 'node'; +const electronBuilderCliPath = path.join(packageRoot, 'node_modules', 'electron-builder', 'cli.js'); +const builderUtilRequire = createRequire( + path.join(packageRoot, 'node_modules', 'builder-util', 'out', 'util.js'), +); +const requiredBuilderRuntimeModules = ['app-builder-bin']; const appBuilderLibVersion = desktopPackageLock.packages?.['node_modules/app-builder-lib']?.version ?? electronBuilderVersion.replace(/^[^\d]*/, ''); const stamp = new Date().toISOString().replace(/[:.]/g, '-'); const outputDir = path.join(releaseRoot, stamp); -const generatedBuilderConfigPath = path.join(outputDir, 'electron-builder.generated.json'); +const latestReleasePointerPath = path.join(releaseRoot, '.latest-unpacked-release'); +const electronBuilderConfigPath = path.join(packageRoot, 'electron-builder.yml'); const requiredNsisTemplateFiles = [ path.join(packageRoot, 'node_modules', 'app-builder-lib', 'templates', 'nsis', 'messages.yml'), path.join( @@ -48,78 +56,23 @@ const artifactFileExtensions = new Set(['.AppImage', '.dmg', '.exe', '.msi', '.z const builderArgs = process.argv.slice(2); const requestedTargets = builderArgs.filter((argument) => argument in supportedTargetHosts); -const createBuilderConfig = () => ({ - appId: 'io.github.abhigyanpatwari.gitnexus.desktop', - electronVersion, - productName: 'GitNexus Desktop', - directories: { - output: outputDir, - buildResources: path.join(packageRoot, 'build'), - }, - files: ['dist/**/*', 'package.json'], - extraResources: [ - { - from: path.join(gitnexusWebRoot, 'dist'), - to: 'gitnexus-web', - filter: ['**/*'], - }, - { - from: path.join(gitnexusRoot, 'dist'), - to: 'gitnexus/dist', - filter: ['**/*'], - }, - { - from: path.join(gitnexusRoot, 'node_modules'), - to: 'gitnexus/node_modules', - filter: ['**/*'], - }, - { - from: path.join(gitnexusRoot, 'hooks'), - to: 'gitnexus/hooks', - filter: ['**/*'], - }, - { - from: path.join(gitnexusRoot, 'skills'), - to: 'gitnexus/skills', - filter: ['**/*'], - }, - { - from: path.join(gitnexusRoot, 'vendor'), - to: 'gitnexus/vendor', - filter: ['**/*'], - }, - { - from: path.join(gitnexusRoot, 'package.json'), - to: 'gitnexus/package.json', - }, - ], - asar: false, - npmRebuild: false, - win: { - signAndEditExecutable: false, - icon: path.join(packageRoot, 'build', 'icon.png'), - target: ['nsis'], - }, - mac: { - category: 'public.app-category.developer-tools', - icon: path.join(packageRoot, 'build', 'icon.png'), - target: ['dmg'], - }, - linux: { - category: 'Development', - icon: path.join(packageRoot, 'build', 'icon.png'), - target: ['AppImage'], - }, - nsis: { - oneClick: false, - allowToChangeInstallationDirectory: true, - }, -}); +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_NODE_EXECUTABLE: process.execPath, + GITNEXUS_DESKTOP_NODE_RESOURCE_PATH: `gitnexus-node/${bundledNodeExecutableName}`, + GITNEXUS_DESKTOP_WEB_DIST: path.join(gitnexusWebRoot, 'dist'), +}; const builderCommand = [ - `npx --yes -p electron@${electronVersion} -p electron-builder@${electronBuilderVersion} electron-builder`, - `--config "${generatedBuilderConfigPath}"`, + `node "${electronBuilderCliPath}"`, + `--config "${electronBuilderConfigPath}"`, `-c.electronVersion=${electronVersion}`, + `-c.directories.output="${outputDir}"`, '--publish never', ...builderArgs, ].join(' '); @@ -144,6 +97,15 @@ const tryRunCommand = (command, cwd, extraEnv = {}) => { } }; +const canResolveBuilderRuntimeModule = (moduleName) => { + try { + builderUtilRequire.resolve(moduleName); + return true; + } catch { + return false; + } +}; + const overlayDirectoryContents = (sourceDirectory, destinationDirectory) => { fs.mkdirSync(destinationDirectory, { recursive: true }); @@ -202,56 +164,62 @@ const assertSupportedHostForRequestedTargets = () => { }; const ensureDesktopToolchainHealthy = () => { - if (!requestedTargets.includes('--win')) { + const missingBuilderRuntimeModules = requiredBuilderRuntimeModules.filter( + (moduleName) => !canResolveBuilderRuntimeModule(moduleName), + ); + const missingNsisTemplates = requestedTargets.includes('--win') + ? requiredNsisTemplateFiles.filter((filePath) => !fs.existsSync(filePath)) + : []; + + if (missingBuilderRuntimeModules.length === 0 && missingNsisTemplates.length === 0) { return; } - const missingNsisTemplates = requiredNsisTemplateFiles.filter( - (filePath) => !fs.existsSync(filePath), - ); + if (missingNsisTemplates.length > 0) { + console.warn( + '[build] electron-builder NSIS templates are missing. Restoring app-builder-lib package contents first...', + ); - if (missingNsisTemplates.length === 0) { + try { + repairAppBuilderLibPackage(); + } catch { + // Fall through to install-based repair paths below. + } + } + + const remainingMissingBuilderRuntimeModules = requiredBuilderRuntimeModules.filter( + (moduleName) => !canResolveBuilderRuntimeModule(moduleName), + ); + const remainingMissingNsisTemplates = requestedTargets.includes('--win') + ? requiredNsisTemplateFiles.filter((filePath) => !fs.existsSync(filePath)) + : []; + + if ( + remainingMissingBuilderRuntimeModules.length === 0 && + remainingMissingNsisTemplates.length === 0 + ) { return; } console.warn( - '[build] electron-builder NSIS templates are missing. Restoring app-builder-lib package contents first...', + '[build] electron-builder installation is incomplete. Restoring desktop dependencies with npm ci...', ); + runCommand('npm ci', packageRoot); - try { - repairAppBuilderLibPackage(); - } catch { - // Fall through to install-based repair paths below. - } - - if (requiredNsisTemplateFiles.every((filePath) => fs.existsSync(filePath))) { - return; - } - - console.warn( - '[build] Package overlay repair was not enough. Reinstalling electron-builder next...', + const unresolvedBuilderRuntimeModules = requiredBuilderRuntimeModules.filter( + (moduleName) => !canResolveBuilderRuntimeModule(moduleName), ); - - const targetedRepairWorked = tryRunCommand( - `npm install --no-save --package-lock=false electron-builder@${electronBuilderVersion}`, - packageRoot, - ); - - const targetedRepairResolvedTemplates = - targetedRepairWorked && requiredNsisTemplateFiles.every((filePath) => fs.existsSync(filePath)); - - if (!targetedRepairResolvedTemplates) { - console.warn('[build] Targeted repair was not enough. Falling back to npm ci...'); - runCommand('npm ci', packageRoot); - } - const unresolvedTemplates = requiredNsisTemplateFiles.filter( (filePath) => !fs.existsSync(filePath), ); - if (unresolvedTemplates.length > 0) { + if (unresolvedBuilderRuntimeModules.length > 0 || unresolvedTemplates.length > 0) { throw new Error( - `electron-builder is missing required NSIS templates after reinstall:\n${unresolvedTemplates.join('\n')}`, + [ + 'electron-builder is missing required runtime modules after reinstall:', + ...unresolvedBuilderRuntimeModules, + ...unresolvedTemplates, + ].join('\n'), ); } }; @@ -282,7 +250,7 @@ const listArtifacts = (directoryPath) => { }; fs.mkdirSync(outputDir, { recursive: true }); -fs.writeFileSync(generatedBuilderConfigPath, JSON.stringify(createBuilderConfig(), null, 2)); +fs.writeFileSync(latestReleasePointerPath, `${outputDir}\n`); assertSupportedHostForRequestedTargets(); ensureDesktopToolchainHealthy(); @@ -293,7 +261,7 @@ runCommand('npm run bundle', packageRoot); runCommand('npm run build', gitnexusWebRoot); -runCommand(builderCommand, packageRoot); +runCommand(builderCommand, packageRoot, builderEnvironment); const artifacts = listArtifacts(outputDir); diff --git a/gitnexus-desktop/scripts/smoke-unpacked.mjs b/gitnexus-desktop/scripts/smoke-unpacked.mjs new file mode 100644 index 000000000..d92229075 --- /dev/null +++ b/gitnexus-desktop/scripts/smoke-unpacked.mjs @@ -0,0 +1,164 @@ +import { spawn } from 'node:child_process'; +import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const packageRoot = path.resolve(__dirname, '..'); +const releaseRoot = path.join(packageRoot, 'release'); +const latestReleasePointerPath = path.join(releaseRoot, '.latest-unpacked-release'); +const productName = 'GitNexus Desktop'; +const smokeTimeoutMs = 120_000; +const releaseDirectoryPattern = /^\d{4}-\d{2}-\d{2}T.*Z$/; + +const listTimestampedReleaseDirs = () => { + if (!existsSync(releaseRoot)) { + return []; + } + + return readdirSync(releaseRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory() && releaseDirectoryPattern.test(entry.name)) + .map((entry) => path.join(releaseRoot, entry.name)) + .sort((leftPath, rightPath) => statSync(rightPath).mtimeMs - statSync(leftPath).mtimeMs); +}; + +const findFirstMatch = (rootDir, predicate) => { + for (const entry of readdirSync(rootDir, { withFileTypes: true })) { + const entryPath = path.join(rootDir, entry.name); + + if (predicate(entry, entryPath)) { + return entryPath; + } + + if (entry.isDirectory()) { + const nestedMatch = findFirstMatch(entryPath, predicate); + + if (nestedMatch) { + return nestedMatch; + } + } + } + + return null; +}; + +const resolveExecutablePath = (releaseDir) => { + if (process.platform === 'win32') { + return path.join(releaseDir, 'win-unpacked', `${productName}.exe`); + } + + if (process.platform === 'darwin') { + return findFirstMatch(releaseDir, (entry, entryPath) => { + return ( + entry.isFile() && + entry.name === productName && + entryPath.includes(`${productName}.app${path.sep}Contents${path.sep}MacOS${path.sep}`) + ); + }); + } + + const linuxUnpackedDir = path.join(releaseDir, 'linux-unpacked'); + + if (!existsSync(linuxUnpackedDir)) { + return null; + } + + const candidateNames = [productName, 'gitnexus-desktop']; + + for (const candidateName of candidateNames) { + const candidatePath = path.join(linuxUnpackedDir, candidateName); + + if (existsSync(candidatePath) && statSync(candidatePath).isFile()) { + return candidatePath; + } + } + + return findFirstMatch(linuxUnpackedDir, (entry, entryPath) => { + return entry.isFile() && path.extname(entryPath) === ''; + }); +}; + +const resolveLatestExecutable = () => { + if (existsSync(latestReleasePointerPath)) { + const releaseDir = readFileSync(latestReleasePointerPath, 'utf8').trim(); + const executablePath = releaseDir ? resolveExecutablePath(releaseDir) : null; + + return { + executablePath: executablePath && existsSync(executablePath) ? executablePath : null, + releaseDir: releaseDir || null, + }; + } + + const releaseDirs = listTimestampedReleaseDirs(); + + for (const releaseDir of releaseDirs) { + const executablePath = resolveExecutablePath(releaseDir); + + if (executablePath && existsSync(executablePath)) { + return { executablePath, releaseDir }; + } + } + + return { + executablePath: null, + releaseDir: releaseDirs[0] ?? null, + }; +}; + +const runSmokeTest = async () => { + const { executablePath, releaseDir: latestReleaseDir } = resolveLatestExecutable(); + + if (!latestReleaseDir) { + throw new Error( + 'No timestamped desktop release directory was found. Run npm run build:dir first.', + ); + } + + if (!executablePath || !existsSync(executablePath)) { + throw new Error(`Unable to locate the unpacked desktop executable under ${latestReleaseDir}.`); + } + + console.info(`[gitnexus-desktop] Smoke testing unpacked app: ${executablePath}`); + + await new Promise((resolve, reject) => { + const childProcess = spawn(executablePath, [], { + cwd: path.dirname(executablePath), + env: { + ...process.env, + GITNEXUS_DESKTOP_SMOKE_TEST: '1', + }, + stdio: 'inherit', + windowsHide: true, + }); + + const timeoutHandle = setTimeout(() => { + childProcess.kill(process.platform === 'win32' ? undefined : 'SIGTERM'); + reject( + new Error(`Timed out waiting for unpacked desktop smoke test after ${smokeTimeoutMs}ms.`), + ); + }, smokeTimeoutMs); + + childProcess.once('error', (error) => { + clearTimeout(timeoutHandle); + reject(error); + }); + + childProcess.once('exit', (code, signal) => { + clearTimeout(timeoutHandle); + + if (code === 0) { + resolve(undefined); + return; + } + + reject( + new Error( + `Unpacked desktop smoke test exited with code ${code ?? 'null'}${signal ? ` (signal: ${signal})` : ''}.`, + ), + ); + }); + }); +}; + +await runSmokeTest(); diff --git a/gitnexus-desktop/src/main/main.ts b/gitnexus-desktop/src/main/main.ts index 08637b469..a226c0364 100644 --- a/gitnexus-desktop/src/main/main.ts +++ b/gitnexus-desktop/src/main/main.ts @@ -57,37 +57,6 @@ const MIME_TYPES: Record = { '.woff2': 'font/woff2', }; -const EMBEDDED_APP_SCROLLBAR_CSS = ` - :root { - scrollbar-color: rgba(148, 163, 184, 0.42) rgba(7, 10, 18, 0.08); - } - - * { - scrollbar-width: thin; - } - - ::-webkit-scrollbar { - width: 12px; - height: 12px; - } - - ::-webkit-scrollbar-track { - background: rgba(7, 10, 18, 0.08); - } - - ::-webkit-scrollbar-thumb { - border: 3px solid transparent; - border-radius: 999px; - background: rgba(148, 163, 184, 0.34); - background-clip: padding-box; - } - - ::-webkit-scrollbar-thumb:hover { - background: rgba(148, 163, 184, 0.52); - background-clip: padding-box; - } -`; - let serverReadyPromise: Promise | null = null; let gitNexusServerProcess: ChildProcess | null = null; let gitNexusServerOutput = ''; @@ -430,20 +399,7 @@ const sendStaticResponse = (assetPath: string | null, response: ServerResponse): const handlePackagedWebRequest = (request: IncomingMessage, response: ServerResponse): void => { const requestUrl = request.url ?? '/'; - let requestedPath: string; - - try { - requestedPath = decodeURIComponent(new URL(requestUrl, 'http://127.0.0.1').pathname); - } catch (error) { - if (error instanceof URIError) { - response.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' }); - response.end('Bad request'); - return; - } - - throw error; - } - + const requestedPath = decodeURIComponent(new URL(requestUrl, 'http://127.0.0.1').pathname); sendStaticResponse(normalizeStaticPath(GITNEXUS_WEB_PACKAGED_DIR, requestedPath), response); }; @@ -533,7 +489,7 @@ const getDesktopShellState = (window: BrowserWindow): DesktopShellState => { isAlwaysOnTop: window.isAlwaysOnTop(), isMaximized: window.isMaximized() || window.isFullScreen(), platform: process.platform, - titleBarHeight: getDesktopShellTitlebarHeight(), + titleBarHeight: DESKTOP_SHELL_TITLEBAR_HEIGHT, }; }; @@ -555,10 +511,6 @@ const getWindowFromSender = (sender: Electron.WebContents): BrowserWindow => { return window; }; -const getDesktopShellTitlebarHeight = (): number => { - return process.platform === 'darwin' ? DESKTOP_SHELL_TITLEBAR_HEIGHT : 0; -}; - const updateContentViewBounds = (window: BrowserWindow): void => { const contentView = contentViews.get(window); @@ -567,13 +519,12 @@ const updateContentViewBounds = (window: BrowserWindow): void => { } const [width, height] = window.getContentSize(); - const titlebarHeight = getDesktopShellTitlebarHeight(); contentView.setBounds({ x: 0, - y: titlebarHeight, + y: DESKTOP_SHELL_TITLEBAR_HEIGHT, width: Math.max(width, 1), - height: Math.max(height - titlebarHeight, 1), + height: Math.max(height - DESKTOP_SHELL_TITLEBAR_HEIGHT, 1), }); }; @@ -604,14 +555,6 @@ const createEmbeddedContentView = (contentUrl: string): BrowserView => { openExternalUrlIfSafe(navigationUrl); }); - contentView.webContents.on('dom-ready', () => { - void contentView.webContents.insertCSS(EMBEDDED_APP_SCROLLBAR_CSS).catch((error) => { - console.warn( - `[gitnexus-desktop] Failed to inject embedded app CSS: ${getErrorMessage(error)}`, - ); - }); - }); - return contentView; }; @@ -624,7 +567,7 @@ const loadShellRenderer = async (window: BrowserWindow): Promise => { return; } - const rendererEntry = path.join(__dirname, '../renderer/index.html'); + const rendererEntry = path.join(__dirname, '../renderer/src/renderer/index.html'); console.info(`[gitnexus-desktop] Loading shell renderer file: ${rendererEntry}`); await window.loadFile(rendererEntry); }; @@ -723,9 +666,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,