Add add-on management: registry browser, install, remove (step 5)

Browse the ~270-entry DDEV add-on registry with search, install via
`ddev addon get`, remove via `ddev addon remove` — both routed through
the streaming commandRunner. `addon get`/`remove` support `--project
<name>` directly (unlike snapshot restore), so no cwd trick needed
here. Verified working with the project stopped, per the plan's
requirement.

Found and fixed a real bug via live UI testing: `ddev addon list
--installed --json-output` omits the `raw` key entirely when nothing
is installed (unlike `ddev list`/`ddev snapshot --list`, which include
`raw: null`). The shared runDdevRead helper treated that as an error,
so after removing the last add-on the query errored on refetch and
React Query kept showing the stale cached row instead of clearing it.
Split the helper into runDdevRead (strict, for describeProject where
missing data is a real error) and runDdevReadList (treats missing raw
as empty, for every list-style command) and fixed all four list call
sites to use it.

Verified end-to-end via CDP-driven clicks against the real scratch
project: search filtering, install (streamed output, confirmed via
`ddev addon list --installed` on the CLI), and remove — including
re-confirming after the fix that the installed-add-ons table actually
clears instead of showing stale data.
This commit is contained in:
R3ap3R
2026-08-03 00:02:51 -05:00
parent 9f67b13a47
commit ebfec5f787
9 changed files with 334 additions and 5 deletions
+12
View File
@@ -1,6 +1,8 @@
import { contextBridge, ipcRenderer, type IpcRendererEvent } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
import type {
DdevAddonRegistryEntry,
DdevInstalledAddon,
DdevProjectDetail,
DdevProjectSummary,
DdevSnapshot,
@@ -51,6 +53,16 @@ const api = {
pickImportFile: (): Promise<string | null> => ipcRenderer.invoke('database:pickImportFile'),
pickExportPath: (defaultFileName: string): Promise<string | null> =>
ipcRenderer.invoke('database:pickExportPath', defaultFileName)
},
addons: {
listRegistry: (): Promise<DdevAddonRegistryEntry[]> =>
ipcRenderer.invoke('addons:listRegistry'),
listInstalled: (name: string): Promise<DdevInstalledAddon[]> =>
ipcRenderer.invoke('addons:listInstalled', name),
install: (operationId: string, name: string, addonRepo: string): Promise<void> =>
ipcRenderer.invoke('addons:install', operationId, name, addonRepo),
remove: (operationId: string, name: string, addonName: string): Promise<void> =>
ipcRenderer.invoke('addons:remove', operationId, name, addonName)
}
}