Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions desktop/src/core/dockerPath.test.ts
Original file line number Diff line number Diff line change
@@ -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('')
})
})
28 changes: 28 additions & 0 deletions desktop/src/core/dockerPath.ts
Original file line number Diff line number Diff line change
@@ -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<string>()
const out: string[] = []
for (const dir of [...extraDirs, ...currentPath.split(':')]) {
if (dir && !seen.has(dir)) {
seen.add(dir)
out.push(dir)
}
}
return out.join(':')
}
5 changes: 4 additions & 1 deletion desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand Down
5 changes: 4 additions & 1 deletion desktop/src/main/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
17 changes: 15 additions & 2 deletions desktop/src/main/runner.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import { spawn } from 'node:child_process'
import { dockerSearchPath } from '../core/dockerPath'

export interface RunResult {
code: number
stdout: string
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 <args>` to completion, capturing output. Never throws on non-zero. */
export function runDocker(args: string[], env?: NodeJS.ProcessEnv): Promise<RunResult> {
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()))
Expand All @@ -21,9 +31,12 @@ export function runDocker(args: string[], env?: NodeJS.ProcessEnv): Promise<RunR

/** Stream `docker <args>` 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()
}
Loading