From d4c6df0f0279e98b26fdfba56f7c14afe4e6c99a Mon Sep 17 00:00:00 2001 From: Kevin-Tucuxi Date: Sat, 13 Jun 2026 17:19:12 -0700 Subject: [PATCH 1/2] fix(desktop): find docker on the GUI PATH + don't crash the log tail on spawn error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Finder-launched macOS app inherits a minimal PATH (/usr/bin:/bin:/usr/sbin:/sbin) that omits /usr/local/bin where Docker Desktop's CLI lives, so spawn('docker') ENOENTs even with Docker installed — surfacing as a main-process crash (streamDocker had no error handler) and a false 'Docker is not installed' in the wizard. Prepend the known docker bin dirs to PATH (tested dockerSearchPath helper) and swallow streamDocker spawn errors so the log tail can't crash the app. Co-Authored-By: Claude Opus 4.8 (1M context) --- desktop/src/core/dockerPath.test.ts | 24 ++++++++++++++++++++++++ desktop/src/core/dockerPath.ts | 28 ++++++++++++++++++++++++++++ desktop/src/main/runner.ts | 17 +++++++++++++++-- 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 desktop/src/core/dockerPath.test.ts create mode 100644 desktop/src/core/dockerPath.ts diff --git a/desktop/src/core/dockerPath.test.ts b/desktop/src/core/dockerPath.test.ts new file mode 100644 index 00000000..0e73b4a7 --- /dev/null +++ b/desktop/src/core/dockerPath.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect } from 'vitest' +import { dockerSearchPath, DOCKER_BIN_DIRS } from './dockerPath' + +describe('dockerSearchPath', () => { + it('prepends the docker bin dirs to the inherited PATH', () => { + const out = dockerSearchPath('/usr/bin:/bin') + expect(out.split(':').slice(0, DOCKER_BIN_DIRS.length)).toEqual(DOCKER_BIN_DIRS) + expect(out.endsWith('/usr/bin:/bin')).toBe(true) + }) + + it('de-duplicates dirs already present in the inherited PATH', () => { + const out = dockerSearchPath('/usr/local/bin:/usr/bin') + expect(out.split(':').filter((d) => d === '/usr/local/bin')).toHaveLength(1) + }) + + it('handles an empty/undefined inherited PATH (GUI-launch minimal env)', () => { + expect(dockerSearchPath('')).toBe(DOCKER_BIN_DIRS.join(':')) + expect(dockerSearchPath()).toBe(DOCKER_BIN_DIRS.join(':')) + }) + + it('drops empty segments', () => { + expect(dockerSearchPath('::/usr/bin:').split(':')).not.toContain('') + }) +}) diff --git a/desktop/src/core/dockerPath.ts b/desktop/src/core/dockerPath.ts new file mode 100644 index 00000000..d0d1aae1 --- /dev/null +++ b/desktop/src/core/dockerPath.ts @@ -0,0 +1,28 @@ +// A macOS app launched from Finder/Applications inherits a minimal PATH +// (/usr/bin:/bin:/usr/sbin:/sbin) that omits the directories where the `docker` +// CLI actually lives — so a bare `spawn('docker')` fails with ENOENT even when +// Docker Desktop is installed. We prepend the known docker bin dirs to PATH. + +/** Standard locations the `docker` CLI is installed to on macOS. */ +export const DOCKER_BIN_DIRS = [ + '/usr/local/bin', // Docker Desktop CLI symlink (Intel + Apple Silicon) + '/opt/homebrew/bin', // Homebrew on Apple Silicon + '/Applications/Docker.app/Contents/Resources/bin' // Docker Desktop bundled CLI +] + +/** + * Merge a base PATH with the known docker bin dirs. Extra dirs come first so a + * Finder-launched app can find `docker`; entries are de-duplicated, order preserved. + * Pure: the caller passes process.env.PATH. + */ +export function dockerSearchPath(currentPath = '', extraDirs: string[] = DOCKER_BIN_DIRS): string { + const seen = new Set() + const out: string[] = [] + for (const dir of [...extraDirs, ...currentPath.split(':')]) { + if (dir && !seen.has(dir)) { + seen.add(dir) + out.push(dir) + } + } + return out.join(':') +} diff --git a/desktop/src/main/runner.ts b/desktop/src/main/runner.ts index a3465084..2faefb04 100644 --- a/desktop/src/main/runner.ts +++ b/desktop/src/main/runner.ts @@ -1,4 +1,5 @@ import { spawn } from 'node:child_process' +import { dockerSearchPath } from '../core/dockerPath' export interface RunResult { code: number @@ -6,10 +7,19 @@ export interface RunResult { stderr: string } +/** + * Env for spawning `docker`. A Finder-launched macOS app inherits a minimal PATH that + * omits /usr/local/bin (where Docker Desktop's CLI lives), so a bare spawn ENOENTs even + * with Docker installed. Augment PATH with the known docker bin dirs. + */ +function dockerSpawnEnv(extra?: NodeJS.ProcessEnv): NodeJS.ProcessEnv { + return { ...process.env, ...extra, PATH: dockerSearchPath(process.env.PATH) } +} + /** Run `docker ` to completion, capturing output. Never throws on non-zero. */ export function runDocker(args: string[], env?: NodeJS.ProcessEnv): Promise { return new Promise((resolve) => { - const child = spawn('docker', args, { env: { ...process.env, ...env } }) + const child = spawn('docker', args, { env: dockerSpawnEnv(env) }) let stdout = '' let stderr = '' child.stdout.on('data', (d) => (stdout += d.toString())) @@ -21,9 +31,12 @@ export function runDocker(args: string[], env?: NodeJS.ProcessEnv): Promise` lines to a callback (for `logs -f`). Returns a kill fn. */ export function streamDocker(args: string[], onLine: (line: string) => void): () => void { - const child = spawn('docker', args) + const child = spawn('docker', args, { env: dockerSpawnEnv() }) const pump = (buf: Buffer) => buf.toString().split('\n').forEach((l) => l && onLine(l)) child.stdout.on('data', pump) child.stderr.on('data', pump) + // Best-effort: a spawn failure (e.g. docker not found, or the stack not up yet) must + // NOT crash the main process — the engine/stack state is reported via runDocker instead. + child.on('error', () => {}) return () => child.kill() } From ed0900615e60a3b3c604faa92581d758d4138318 Mon Sep 17 00:00:00 2001 From: Kevin-Tucuxi Date: Sat, 13 Jun 2026 17:38:46 -0700 Subject: [PATCH 2/2] fix(desktop): isolate launcher to its own compose project + persist config only after success MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (1) PROJECT_NAME donna→donna-desktop: the launcher shared project 'donna' with the build-from-source/raw-lq-ai dev stacks, so it reused their postgres volume — whose password differs from the launcher's generated one → api 'password authentication failed' crash-loop. A distinct project gives the launcher its own isolated volumes. (2) saveConfig only after the stack is healthy + admin created, so a failed first run re-shows the wizard instead of stranding a half-configured install. Co-Authored-By: Claude Opus 4.8 (1M context) --- desktop/src/main/index.ts | 5 ++++- desktop/src/main/paths.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/desktop/src/main/index.ts b/desktop/src/main/index.ts index 839dba6e..42d8043a 100644 --- a/desktop/src/main/index.ts +++ b/desktop/src/main/index.ts @@ -70,12 +70,15 @@ ipcMain.handle('wizard:complete', async (_e, input: WizardInput) => { inference: input.inference, adminEmail: input.adminEmail } - saveConfig(cfg) + // Write the .env (needed before startStack) but DON'T persist the config blob yet — + // only mark the wizard complete after the stack is healthy and the admin exists, so a + // failed first run re-shows the wizard instead of stranding a half-configured install. writeEnvFile(cfg) const b = base() await startStack(b, process.env) await waitHealthy(b) await runAdminFixture(b, input.adminEmail, input.adminPassword) + saveConfig(cfg) return { ok: true } } catch (err) { return { ok: false, error: String(err) } diff --git a/desktop/src/main/paths.ts b/desktop/src/main/paths.ts index b14480b1..1e92208f 100644 --- a/desktop/src/main/paths.ts +++ b/desktop/src/main/paths.ts @@ -19,4 +19,7 @@ export const composeFilePath = (): string => ? join(process.resourcesPath, 'docker-compose.release.yml') : join(app.getAppPath(), '..', 'docker-compose.release.yml') -export const PROJECT_NAME = 'donna' +// Distinct from the build-from-source / raw-lq-ai dev stacks (which use project "donna") +// so the launcher gets its OWN isolated volumes and never collides on volumes/ports. +// `-p` overrides the compose file's top-level `name:`. +export const PROJECT_NAME = 'donna-desktop'