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
4 changes: 4 additions & 0 deletions desktop/src/core/compose.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
psArgs,
upArgs,
downArgs,
downVArgs,
logsArgs,
adminFixtureArgs
} from './compose'
Expand Down Expand Up @@ -33,6 +34,9 @@ describe('argv builders', () => {
it('down keeps volumes (no -v) so user data survives a stop', () => {
expect(downArgs(base)).toEqual([...base, 'down'])
})
it('down -v also removes volumes (Reset)', () => {
expect(downVArgs(base)).toEqual([...base, 'down', '-v'])
})
it('logs follow a single service', () => {
expect(logsArgs(base, 'donna-web')).toEqual([...base, 'logs', '-f', '--tail', '200', 'donna-web'])
})
Expand Down
2 changes: 2 additions & 0 deletions desktop/src/core/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export function composeBaseArgs(composeFile: string, projectName: string): strin
export const psArgs = (base: string[]): string[] => [...base, 'ps', '--format', 'json']
export const upArgs = (base: string[]): string[] => [...base, 'up', '-d']
export const downArgs = (base: string[]): string[] => [...base, 'down']
/** `down -v` — also removes volumes. Used by Reset to wipe all data for a fresh setup. */
export const downVArgs = (base: string[]): string[] => [...base, 'down', '-v']
export const logsArgs = (base: string[], service: string): string[] => [
...base,
'logs',
Expand Down
25 changes: 22 additions & 3 deletions desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,16 @@ import { resolvePorts } from '../core/ports'
import { generateSecrets } from '../core/secrets'
import { DEFAULT_PORTS } from '../core/types'
import type { InferenceChoice, LauncherConfig } from '../core/config'
import { loadConfig, saveConfig, writeEnvFile } from './store'
import { loadConfig, saveConfig, writeEnvFile, clearConfig } from './store'
import { composeFilePath, envPath, PROJECT_NAME } from './paths'
import { snapshot, startStack, stopStack, runAdminFixture, type StackSnapshot } from './orchestrator'
import {
snapshot,
startStack,
stopStack,
resetStack,
runAdminFixture,
type StackSnapshot
} from './orchestrator'
import { streamDocker } from './runner'
import { isPortFreeSync } from './netcheck'

Expand Down Expand Up @@ -98,8 +105,20 @@ ipcMain.handle('stack:openDonna', () => {
const port = cfg?.ports.donnaWeb ?? DEFAULT_PORTS.donnaWeb
win?.loadURL(`http://localhost:${port}`)
})
// Reset: stop the stack, remove its volumes (down -v), and delete the stored config/.env
// so the next launch re-runs the first-run wizard. down -v runs while .env still exists.
ipcMain.handle('stack:reset', async () => {
try {
await resetStack(base())
clearConfig()
return { ok: true }
} catch (err) {
return { ok: false, error: String(err) }
}
})
ipcMain.handle('engine:installDocker', () =>
shell.openExternal('https://www.docker.com/products/docker-desktop/')
// Direct Apple-Silicon Docker Desktop download (this launcher is arm64-only).
shell.openExternal('https://desktop.docker.com/mac/main/arm64/Docker.dmg')
)

app.whenReady().then(() => {
Expand Down
5 changes: 4 additions & 1 deletion desktop/src/main/orchestrator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { parseEngineProbe } from '../core/engine'
import { parseComposePs, psArgs, upArgs, downArgs, adminFixtureArgs } from '../core/compose'
import { parseComposePs, psArgs, upArgs, downArgs, downVArgs, adminFixtureArgs } from '../core/compose'
import { deriveLauncherState } from '../core/state'
import type { LauncherState, ServiceStatus } from '../core/types'
import { runDocker, type RunResult } from './runner'
Expand Down Expand Up @@ -29,6 +29,9 @@ export const startStack = (base: string[], env: NodeJS.ProcessEnv): Promise<RunR

export const stopStack = (base: string[]): Promise<RunResult> => runDocker(downArgs(base))

/** Reset: stop the stack AND remove its volumes (wipes all data) for a fresh setup. */
export const resetStack = (base: string[]): Promise<RunResult> => runDocker(downVArgs(base))

export const runAdminFixture = (
base: string[],
email: string,
Expand Down
9 changes: 8 additions & 1 deletion desktop/src/main/store.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { safeStorage } from 'electron'
import { writeFileSync, readFileSync, existsSync, chmodSync } from 'node:fs'
import { writeFileSync, readFileSync, existsSync, chmodSync, rmSync } from 'node:fs'
import { configPath, envPath } from './paths'
import { renderEnv } from '../core/env'
import type { LauncherConfig } from '../core/config'
Expand All @@ -23,6 +23,13 @@ export function loadConfig(): LauncherConfig | null {
return JSON.parse(json) as LauncherConfig
}

/** Delete the persisted config + .env so the next launch re-runs the first-run wizard. */
export function clearConfig(): void {
for (const p of [configPath(), envPath()]) {
if (existsSync(p)) rmSync(p)
}
}

/** Write the chmod-600 .env the compose command reads, into the app data dir. */
export function writeEnvFile(cfg: LauncherConfig): string {
const path = envPath()
Expand Down
1 change: 1 addition & 0 deletions desktop/src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const api = {
status: (): Promise<unknown> => ipcRenderer.invoke('stack:status'),
start: (): Promise<unknown> => ipcRenderer.invoke('stack:start'),
stop: (): Promise<unknown> => ipcRenderer.invoke('stack:stop'),
reset: (): Promise<{ ok: boolean; error?: string }> => ipcRenderer.invoke('stack:reset'),
openDonna: (): Promise<void> => ipcRenderer.invoke('stack:openDonna'),
installDocker: (): Promise<void> => ipcRenderer.invoke('engine:installDocker'),
onLog: (cb: (line: string) => void): void => {
Expand Down
42 changes: 39 additions & 3 deletions desktop/src/renderer/panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,62 @@ export function renderPanel(root: HTMLElement): void {
</div>
<h3>Logs</h3>
<div id="logs"></div>
<p style="margin-top:24px">
<button id="reset" class="secondary">Reset…</button>
<span id="resethint" style="color:#555; margin-left:8px"></span>
</p>
`
const stateEl = document.getElementById('state')!
const logsEl = document.getElementById('logs')!
const open = document.getElementById('open') as HTMLButtonElement
const msgEl = document.getElementById('msg')!
const install = document.getElementById('install') as HTMLButtonElement
const reset = document.getElementById('reset') as HTMLButtonElement
const resetHint = document.getElementById('resethint')!

const apply = (snap: Snapshot): void => {
stateEl.textContent = LABELS[snap.state] ?? snap.state
let label = LABELS[snap.state] ?? snap.state
if (snap.state === 'STACK_STARTING') {
const healthy = (snap.services ?? []).filter((s) => s.health === 'healthy').length
label = `Starting… ${healthy}/8 services ready`
}
stateEl.textContent = label
open.disabled = snap.state !== 'HEALTHY'
const noEngine = snap.state === 'NO_ENGINE'
msgEl.textContent = noEngine ? (snap.engineMessage ?? '') : ''
install.style.display = noEngine ? 'inline-block' : 'none'
}

document.getElementById('open')!.addEventListener('click', () => window.donna.openDonna())
document.getElementById('start')!.addEventListener('click', () => window.donna.start())
document.getElementById('stop')!.addEventListener('click', () => window.donna.stop())
document.getElementById('start')!.addEventListener('click', async () => {
await window.donna.start()
tick()
})
document.getElementById('stop')!.addEventListener('click', async () => {
await window.donna.stop()
tick()
})
install.addEventListener('click', () => window.donna.installDocker())

// Reset wipes the stack + all data and re-runs first-run setup — two-click confirm.
let resetArmed = false
reset.addEventListener('click', async () => {
if (!resetArmed) {
resetArmed = true
resetHint.textContent = 'This erases all Donna data on this Mac. Click again to confirm.'
return
}
reset.disabled = true
resetHint.textContent = 'Resetting…'
const res = await window.donna.reset()
if (res.ok) window.location.reload()
else {
reset.disabled = false
resetArmed = false
resetHint.textContent = res.error ?? 'Reset failed.'
}
})

const MAX_LOG_LINES = 2000
window.donna.onLog((line) => {
const lines = (logsEl.textContent + line + '\n').split('\n')
Expand Down
10 changes: 7 additions & 3 deletions docs/INSTALL-MAC.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ terminal, no GitHub, no config files**. This guide walks through it with picture
> **What you need**
> - A **Mac with Apple Silicon** (M1/M2/M3/M4). *(An Intel build is coming.)*
> - **Docker Desktop** installed and running — Donna uses it to run its private engine on your
> machine. Get it free at [docker.com/products/docker-desktop](https://www.docker.com/products/docker-desktop/).
> (If Docker isn't installed, Donna will tell you and link you to it.)
> machine. **[Download Docker Desktop for Apple Silicon →](https://desktop.docker.com/mac/main/arm64/Docker.dmg)**
> (or browse [docker.com/products/docker-desktop](https://www.docker.com/products/docker-desktop/)).
> If Docker isn't installed, Donna links you straight to it.
> - **~12 GB of free disk** for the engine images, and an internet connection for the first run.

Everything Donna does runs **locally on your Mac**. Your documents never leave your computer unless
Expand Down Expand Up @@ -44,7 +45,8 @@ The first time you open Donna, a short **Welcome** screen appears. Two quick cho
- **Use a cloud API key (recommended)** — paste an Anthropic API key for the best quality and speed.
*(Optional — you can leave it blank and add a key later in Settings.)*
- **Run fully local with Ollama** — keep everything on your Mac with no cloud at all. This needs
[Ollama](https://ollama.com) installed and running with a model pulled (e.g. `ollama pull qwen2.5`).
**[Ollama](https://ollama.com/download)** installed and running with a model pulled (e.g.
`ollama pull qwen2.5`).

**2. Set your password** — your login is **`admin@lq.ai`** (shown on screen); choose a password of at
least 12 characters. You can change the email and password later in **Settings → Account**.
Expand Down Expand Up @@ -85,6 +87,8 @@ The Donna app window is your **control panel**:
- **Start / Stop** — start or stop the engine. Stopping frees up your Mac's resources; your data is
kept and is there next time you Start.
- **Logs** — a live view of what the engine is doing, handy if something looks stuck.
- **Reset…** — erases all Donna data on this Mac and re‑runs first‑time setup (two clicks to confirm).
Use this only if you want to start completely fresh.

You can quit the app when you're done. Re‑opening it goes straight to the control panel — the setup
wizard only runs the very first time.
Expand Down
Loading