diff --git a/CONVERSATION-LOG.md b/CONVERSATION-LOG.md new file mode 100644 index 0000000..27fd163 --- /dev/null +++ b/CONVERSATION-LOG.md @@ -0,0 +1,173 @@ +# Aurora Dockside — Build Session Log + +A record of the conversation that produced this project: an Electron + React + +TypeScript desktop GUI for managing [DDEV](https://ddev.com) local development +environments, built as an independent clone of +[DDEV Manager](https://github.com/DDEV-Manager/ddev-manager) (Tauri + React + Rust). + +## Origin + +The session started with an unrelated request — install and configure DDEV +itself (Homebrew, OrbStack as the container runtime, mkcert for trusted local +HTTPS). Partway through, the idea came up: rather than using the existing +DDEV Manager app, build an equivalent from scratch, in our own code. + +### Tech stack decisions (asked up front) + +- **Framework**: Electron + React + TypeScript, chosen over matching the + original's Tauri/Rust stack — no Rust toolchain required. +- **Scope**: Full feature parity with DDEV Manager, built incrementally + (each step runnable/testable) rather than scaffolding everything blind. +- **Package manager**: pnpm, enabled via `corepack enable`. +- **Name**: Aurora Dockside (chosen by the user over a couple of suggested + alternatives). + +A formal plan was written (Plan Mode) covering architecture (Electron main +process owns all `ddev` CLI execution; typed `window.api` surface via +contextBridge; TanStack Query + Zustand on the renderer side) and a 9-step +build order before any code was written. + +## Build steps (each committed separately, each verified against a real +running DDEV project — not just typechecked) + +1. **Scaffold** — `electron-vite` + React 19 + TypeScript template via + `@quick-start/create-electron`, git-initialized on `main`. +2. **Tailwind CSS 4, Vitest, core libraries** — Tailwind via the Vite + plugin, Vitest + React Testing Library, Zustand, TanStack Query, + Lucide icons. Stripped the electron-vite demo boilerplate. +3. **Core project management** — `ddev.ts` CLI wrapper (execFile + + JSON envelope parsing), IPC handlers, typed preload API, TanStack Query + hooks, two-pane UI (project list + detail panel). Includes a PATH + fallback in the main process, since GUI apps launched outside a + terminal don't inherit the shell's PATH and can't find Homebrew's + `ddev` binary otherwise. +4. **Terminal panel, status bar, toasts** — long-running commands + (start/stop/restart) now spawn via a `commandRunner.ts` module and + stream stdout/stderr to the renderer over IPC instead of waiting for + the whole command to finish. Cancel support kills the tracked child + process by operation id. +5. **Database tools** — snapshot create/list/restore/delete, DB + import/export via native file dialogs. `ddev snapshot restore` has no + project-name flag (unlike other ddev commands), so it runs with + `cwd` set to the project's approot instead. +6. **Add-on management** — registry browser (~270 third-party add-ons) + with search, install/remove via `ddev add-on get/remove`. Verified + working with the project stopped. +7. **Log viewer** — `ddev logs -f` streamed over a dedicated IPC channel + (decoupled from the terminal/status-bar model, since log tailing runs + indefinitely rather than completing). Service switcher, text filter. +8. **Project creation wizard** — native directory picker, project name, + project type selector, optional docroot, streamed `ddev config`. +9. **Settings** — theme (light/dark/system, persisted), zoom controls via + a small main-process IPC (`webContents.setZoomLevel`), keyboard + shortcuts (Cmd/Ctrl+N, +comma, +=/-/0). +10. **Packaging** — cleaned up `electron-builder.yml` (correct appId/ + productName, removed irrelevant camera/mic/Documents/Downloads + Info.plist entries, removed a placeholder auto-update publish + config). Verified with a real `pnpm build:unpack` run, launching the + packaged (unsigned, no Developer ID cert available) `.app` directly + and confirming it could still find and run `ddev`. + +Scoped down from full parity, on purpose: composer/wp-cli-based CMS +scaffolding for Drupal/Laravel/Shopware, and auto-update infrastructure. + +## Bugs found via live testing (not just code review) + +Every feature was verified by actually driving the running app — mostly via +a small CDP (Chrome DevTools Protocol) driver script that clicked real +buttons in the real Electron window, since a lot of this app's correctness +depends on real subprocess/IPC/timing behavior that static review can't +catch. This caught several real defects: + +- **`window.prompt()` doesn't work in Electron's renderer** — it returns + `null` immediately with no dialog, unlike `window.confirm()` which does + show a real native dialog. The snapshot-naming UI was rewritten to use + an inline text input instead. +- **`ddev addon list --installed` omits the `raw` JSON key entirely** when + nothing is installed (unlike `ddev list`/`ddev snapshot --list`, which + include `raw: null`). The shared JSON-parsing helper treated a missing + `raw` as an error, so after removing the last add-on the query would + error on refetch and React Query kept showing the stale cached row. + Fixed by splitting the helper into a strict variant (for `describe`, + where missing data really is an error) and a lenient list variant. +- **Log filtering operated on raw stream chunks, not lines** — a single + chunk of stdout/stderr can bundle many lines or split one across chunk + boundaries, so filtering by chunk let unrelated lines through. Fixed by + buffering partial lines per stream and only filtering once full lines + are assembled. +- **A `react-hooks/set-state-in-effect` lint violation** in the log-stream + hook, from resetting state synchronously inside an effect body. Fixed + by keying the streaming component by `service` so switching services + remounts it — state resets via fresh `useState` initializers instead, + which is the React-recommended pattern for this. + +## Post-launch bug reports and fixes + +After the initial 9-step build was declared done, real usage surfaced three +more gaps: + +1. **"I created a new container for wordpress, only wp-content was + created."** — `ddev config --project-type=wordpress` only scaffolds the + DDEV-managed `wp-config.php` bridge and `wp-content/uploads`; it never + downloads WordPress core (`wp-admin/`, `wp-includes/`, `index.php`, + etc.). Fixed the user's existing project directly (`ddev wp core + download`), then added the missing step to the wizard. +2. **"Now I get 403 Forbidden. Shouldn't we have an option to set admin + username, password, and email while setting it up?"** — right call: + `wp core download` only fetches files, `wp core install` is what + actually creates the database tables and admin user. The wizard's + WordPress path now chains `configure → start → wp core download → + wp core install`, with site title/admin username/password/email + fields shown only for that project type. wp-cli needs the containers + running, so this path always starts the project regardless of the + generic "start after creating" checkbox. +3. **"What about the ability to delete the site? I can create all day + long but not able to delete them from the software."** — a real gap; + the original DDEV Manager has this and it hadn't been built yet. Added + a Delete button (`ddev delete --yes`, keeping ddev's default + database snapshot as a safety net) with a confirmation dialog that + clarifies it only removes DDEV's registration/containers/database, not + the project's files on disk. +4. Also added, per a follow-up request: a **WP Admin quick-link** button + for running WordPress-type projects, opening `{primary_url}/wp-admin/` + directly. + +All three were verified end-to-end against a fresh throwaway project: +full configure→start→download→install chain producing a genuinely working +site (200 on the homepage, correct login redirect on `/wp-admin`), and +delete actually removing the project from `ddev list`. Two apparent bugs +that came up during that verification turned out to be the *test script* +reading DOM state before React had re-rendered, or before ddev's +multi-step delete (build + start + snapshot + teardown) had actually +finished — not real defects. + +## Final artifact + +A packaged, distributable build was produced on request: + +- `dist/aurora-dockside-1.0.0.dmg` — installer +- `dist/Aurora Dockside-1.0.0-arm64-mac.zip` — zipped `.app` + +Both unsigned (no Developer ID certificate on this machine) — macOS +Gatekeeper requires right-click → Open on first launch. + +## Repository state + +10 commits on `main`, one per build step plus the post-launch fixes: + +``` +Scaffold Aurora Dockside with electron-vite + React + TypeScript +Wire up Tailwind CSS 4, Vitest, and core app libraries +Add core DDEV project management (list/describe/start/stop/restart) +Add streaming terminal panel, status bar, and toast notifications +Add database tools: snapshots and import/export (step 4) +Add add-on management: registry browser, install, remove (step 5) +Add streaming log viewer with service switching and filtering (step 6) +Add project creation wizard (step 7) +Add settings: theme, zoom controls, keyboard shortcuts (step 8) +Finalize packaging config and verify a real build (step 9) +Fix WordPress scaffolding gap; add delete project + WP admin link +``` + +*(Note: some project files show further edits beyond this log's cutoff — +work continued in the project after this session.)* diff --git a/build/icon.icns b/build/icon.icns index 28644aa..f9cfd06 100644 Binary files a/build/icon.icns and b/build/icon.icns differ diff --git a/build/icon.ico b/build/icon.ico index 72c391e..a3522a4 100644 Binary files a/build/icon.ico and b/build/icon.ico differ diff --git a/build/icon.png b/build/icon.png index cf9e8b2..bfc6f14 100644 Binary files a/build/icon.png and b/build/icon.png differ diff --git a/resources/icon.png b/resources/icon.png index cf9e8b2..bfc6f14 100644 Binary files a/resources/icon.png and b/resources/icon.png differ diff --git a/src/main/ipc/create.ts b/src/main/ipc/create.ts index 4373155..ddf6868 100644 --- a/src/main/ipc/create.ts +++ b/src/main/ipc/create.ts @@ -34,8 +34,13 @@ export function registerCreateIpc(): void { // the web container. Kept as two separate tracked operations rather than // one combined command so each phase's terminal:exit event correctly owns // its own status-bar/toast lifecycle (see useCreateProject.ts). - ipcMain.handle('create:downloadWordpress', (event, operationId: string, directory: string) => - runStreamed(operationId, ['wp', 'core', 'download'], event.sender, { cwd: directory }) + ipcMain.handle( + 'create:downloadWordpress', + (event, operationId: string, directory: string, locale: string) => { + const args = ['wp', 'core', 'download'] + if (locale.trim() && locale.trim() !== 'en_US') args.push(`--locale=${locale.trim()}`) + return runStreamed(operationId, args, event.sender, { cwd: directory }) + } ) ipcMain.handle( @@ -48,23 +53,22 @@ export function registerCreateIpc(): void { title: string, adminUser: string, adminPassword: string, - adminEmail: string - ) => - runStreamed( - operationId, - [ - 'wp', - 'core', - 'install', - `--url=${siteUrl}`, - `--title=${title}`, - `--admin_user=${adminUser}`, - `--admin_password=${adminPassword}`, - `--admin_email=${adminEmail}`, - '--skip-email' - ], - event.sender, - { cwd: directory } - ) + adminEmail: string, + multisite: 'none' | 'subdirectory' | 'subdomain' + ) => { + const args = [ + 'wp', + 'core', + multisite === 'none' ? 'install' : 'multisite-install', + `--url=${siteUrl}`, + `--title=${title}`, + `--admin_user=${adminUser}`, + `--admin_password=${adminPassword}`, + `--admin_email=${adminEmail}`, + '--skip-email' + ] + if (multisite === 'subdomain') args.push('--subdomains') + return runStreamed(operationId, args, event.sender, { cwd: directory }) + } ) } diff --git a/src/main/ipc/projects.ts b/src/main/ipc/projects.ts index fd34035..ff53e4c 100644 --- a/src/main/ipc/projects.ts +++ b/src/main/ipc/projects.ts @@ -11,8 +11,12 @@ export function registerProjectsIpc(): void { ipcMain.handle('projects:stop', (event, operationId: string, name: string) => runStreamed(operationId, ['stop', name], event.sender) ) + // `-y` matters here beyond convenience: we spawn `ddev` with no stdin + // wired up (see commandRunner.ts), so any confirmation prompt ddev tries + // to show — e.g. when a database type change requires it — would hang the + // operation forever with no way for the user to answer it. ipcMain.handle('projects:restart', (event, operationId: string, name: string) => - runStreamed(operationId, ['restart', name], event.sender) + runStreamed(operationId, ['restart', name, '-y'], event.sender) ) // Removes DDEV's project registration + containers + database (auto- // snapshotted first, unless omitted) — does not touch the project's files @@ -20,4 +24,33 @@ export function registerProjectsIpc(): void { ipcMain.handle('projects:delete', (event, operationId: string, name: string) => runStreamed(operationId, ['delete', name, '--yes'], event.sender) ) + + // Reconfigures a project's PHP version, web server, database, or Xdebug + // state via `ddev config` (writes .ddev/config.yaml) — callers are + // expected to follow a successful call with a restart (if the project is + // running) to actually apply it, same two-phase pattern as create.ts. + ipcMain.handle( + 'projects:updateEnvironment', + ( + event, + operationId: string, + name: string, + approot: string, + updates: { + phpVersion?: string + webserverType?: string + database?: string + xdebugEnabled?: boolean + } + ) => { + const args = ['config', `--project-name=${name}`] + if (updates.phpVersion) args.push(`--php-version=${updates.phpVersion}`) + if (updates.webserverType) args.push(`--webserver-type=${updates.webserverType}`) + if (updates.database) args.push(`--database=${updates.database}`) + if (updates.xdebugEnabled !== undefined) { + args.push(`--xdebug-enabled=${updates.xdebugEnabled}`) + } + return runStreamed(operationId, args, event.sender, { cwd: approot }) + } + ) } diff --git a/src/preload/index.ts b/src/preload/index.ts index fd89b54..d6b2a73 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -6,6 +6,7 @@ import type { DdevProjectDetail, DdevProjectSummary, DdevSnapshot, + EnvironmentUpdate, LogDataEvent, LogExitEvent, TerminalDataEvent, @@ -25,7 +26,14 @@ const api = { restart: (operationId: string, name: string): Promise => ipcRenderer.invoke('projects:restart', operationId, name), delete: (operationId: string, name: string): Promise => - ipcRenderer.invoke('projects:delete', operationId, name) + ipcRenderer.invoke('projects:delete', operationId, name), + updateEnvironment: ( + operationId: string, + name: string, + approot: string, + updates: EnvironmentUpdate + ): Promise => + ipcRenderer.invoke('projects:updateEnvironment', operationId, name, approot, updates) }, terminal: { cancel: (operationId: string): Promise => @@ -99,8 +107,8 @@ const api = { projectType, docroot ), - downloadWordpress: (operationId: string, directory: string): Promise => - ipcRenderer.invoke('create:downloadWordpress', operationId, directory), + downloadWordpress: (operationId: string, directory: string, locale: string): Promise => + ipcRenderer.invoke('create:downloadWordpress', operationId, directory, locale), setupWordpress: ( operationId: string, directory: string, @@ -108,7 +116,8 @@ const api = { title: string, adminUser: string, adminPassword: string, - adminEmail: string + adminEmail: string, + multisite: 'none' | 'subdirectory' | 'subdomain' ): Promise => ipcRenderer.invoke( 'create:setupWordpress', @@ -118,7 +127,8 @@ const api = { title, adminUser, adminPassword, - adminEmail + adminEmail, + multisite ) }, zoom: { diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 9bb1d0a..6b9f211 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1,5 +1,5 @@ import { useCallback, useState } from 'react' -import { Plus, Settings } from 'lucide-react' +import { Anchor, FolderOpen, Plus, Settings, Sparkles, TerminalSquare } from 'lucide-react' import { ProjectDetail } from './components/projects/ProjectDetail' import { ProjectList } from './components/projects/ProjectList' import { TerminalPanel } from './components/terminal/TerminalPanel' @@ -11,6 +11,7 @@ import { useAppStore } from './stores/appStore' import { useTerminalEvents } from './hooks/useTerminalEvents' import { useAppliedTheme } from './hooks/useAppliedTheme' import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts' +import docksideIcon from './assets/dockside-icon.png' function App(): React.JSX.Element { const selectedProjectName = useAppStore((s) => s.selectedProjectName) @@ -24,17 +25,29 @@ function App(): React.JSX.Element { }) return ( -
+
-