Skip to content
Open
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
6 changes: 4 additions & 2 deletions .docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ make install

The toolchain via mise (node, go, composer and npm deps, the WebKit the browser checks
drive), plus the pinned Adminer and frankenphp downloads. On Linux it also runs
`make linux-deps` — the one step that needs apt, for the GTK/WebKit dev headers the webview
links against.
`make linux-deps`, for the GTK/WebKit dev headers the webview links against: those are a
distro package rather than a toolchain, so it installs them with apt where it can and
otherwise names what to install (Arch: `gtk3 webkit2gtk-4.1`; Fedora: `gtk3-devel
webkit2gtk4.1-devel`) and stops. Headers already present, and it is a no-op on any distro.

## Building and running

Expand Down
10 changes: 6 additions & 4 deletions .docs/releases-linux.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,13 @@ Skipped: signing, arm64, and a tag-push trigger — that last one is four lines

Not for Debian. Debian is served by stage 1. Flatpak buys three things:

1. **Every other distro.** `make linux-deps` refuses anything without apt, and says so.
Fedora, Arch, openSUSE and Silverblue users currently have a tarball and their own
luck with GTK/WebKit versions.
1. **Every other distro.** A checkout builds on Fedora, Arch and openSUSE — `make
linux-deps` names the headers to install where it cannot install them itself — but
there is nothing to *ship* them: those users have a tarball and their own luck with
GTK/WebKit versions.
2. **The webkit2gtk version chase ends.** The runtime pins GTK3 and WebKitGTK, so the
`webkit2gtk-4.0` → `4.1` shim `linux-deps` installs stops being a per-distro problem.
`webkit2gtk-4.0` → `4.1` shim the build writes into `.cache/pkgconfig` stops being a
per-distro problem.
3. **Auto-updates**, which a downloaded `.deb` will never have.

### Repackage, do not rebuild
Expand Down
1 change: 1 addition & 0 deletions .docs/threats.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Who we assume is hostile:
| **Local process holds report connections open** to exhaust sockets and stop the gate hearing a sign-out | The last session lent on after sign-out | A deadline per connection; `Accept` errors are waited out | `launcher/mcpgate/mcpgate_test.go` |
| **Another Windows account connects to the bridge's socket**: Windows lets anyone traverse a directory, so the directory's ACL alone stops nothing | The token and the agent's SQL | The gate's directory is created private and its files inherit that; the bridge checks both | `launcher/mcpbridge`, `windows-ci` workflow |
| **Another Windows account reads the token from `mcp.json`**, whose `chmod` does nothing there, in a data dir outside the profile | The window's session, if the token alone opened the gate | The token only works over that private socket: nothing listens on the URL's port | `launcher/mcpbridge`, `features/shell/mcp.feature` |
| **An Omarchy theme from a git repository puts markup in `colors.toml`**, which reaches an inline `<style>` | Script in the page that holds the session | A value is six hex digits or it is dropped | `tests/cli/OmarchyThemeTest.php` |
| **Agent writes on postgres through a read tool**, including a `COMMIT` smuggled into the query | Changed data with writes switched off | Adminer's untrusted-SQL guard and a read-only transaction | `features/shell/mcp.feature` |
| **Agent changes rows on MySQL through a read tool** | Changed data with writes switched off | The statement runs in a transaction that is rolled back. A schema change escapes it, see Accepted | `features/mysql/mcp-writes.feature`, an `INSERT` only |
| **Agent writes to a connection marked Prod** with writes allowed | Changed production data | `Desktop\Mcp\Endpoint` keeps Prod read-only whatever the setting says. On MySQL only as far as the row above | `features/shell/mcp.feature` |
Expand Down
10 changes: 9 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,20 @@ this machine → Adminer Desktop — giving a real console, the DOM, network and
breakpoints. It also logs diagnostics at startup (which UI delegate is attached, whether
the inspector came up).

On Linux it is **WebKitGTK's own inspector** (`inspector_linux.go`), which `-debug` makes
available without opening — as on macOS, where the page only appears in Safari's Develop
menu. **F12 or Ctrl+Shift+I** opens it, in a window of its own, and reopens it after a
close; the key is taken on the GtkWindow, so it works with the caret in an SQL textarea
too. Every `console.log` and JavaScript error goes to stdout and so into the log whether
the inspector is open or not, which is what you have when the page dies before you get to
it — and what makes `-debug` worth running when you only want the app dressed as another OS.

Reach for it as soon as a symptom is visual or behavioural. `uiDelegate=(nil)` in that
log found in one line what two rounds of guessing had missed.

Never enable the inspector outside `-debug`.

`make debug` asks which OS to look like, so the Windows or Linux theme can be checked from a Mac. It sets `ADMINER_DESKTOP_SIMULATE_OS`, ignored without `-debug`. It changes the look and the window only (`Os::look()`): the engine stays WKWebView, and without Segoe the font falls back.
`make debug` asks which OS to look like, so the Windows or Linux theme can be checked from a Mac. A Linux flavour can be picked too (`LinuxDistro`). It sets `ADMINER_DESKTOP_SIMULATE_OS`, ignored without `-debug`. It changes the look and the window only (`Os::look()`): the engine stays WKWebView, and without Segoe the font falls back.

PHP fatal errors reach `~/Library/Logs/Adminer Desktop/adminer-desktop.log`, but only
because `app/php/desktop.ini` turns `log_errors` on — frankenphp's default sends them to
Expand Down
62 changes: 46 additions & 16 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,25 @@ else
EXE = .exe
endif

# webview is cgo and hardcodes `pkg-config webkit2gtk-4.0`, a .pc no current distro ships:
# Ubuntu dropped it in 24.04 and Arch never had it, both carrying webkit2gtk-4.1 instead —
# the same API over libsoup3, and what download_linux.go already asks for by name. A .pc of
# our own that requires 4.1 is what satisfies webview, and it lives in .cache with
# PKG_CONFIG_PATH pointing cgo at it rather than in /usr: writing there needs root, and it
# made `make build` on a working checkout depend on a sudo someone had run once, months ago.
# Here it is a build artifact like any other, so every distro gets the same 4.1 build and no
# build step needs root. Exported, because every go command in this file compiles the cgo
# launcher — `build`, `go vet`, golangci — not just the one that links it.
ifeq ($(UNAME_S),Linux)
WEBKIT_SHIM = .cache/pkgconfig/webkit2gtk-4.0.pc
export PKG_CONFIG_PATH := $(CURDIR)/.cache/pkgconfig$(if $(PKG_CONFIG_PATH),:$(PKG_CONFIG_PATH))
endif

.cache/pkgconfig/webkit2gtk-4.0.pc:
@mkdir -p $(dir $@)
@printf 'Name: webkit2gtk-4.0\nDescription: Shim onto webkit2gtk-4.1 for webview_go\nVersion: 2.44.0\nRequires: webkit2gtk-4.1\n' > $@
@echo "wrote $@ (webview asks for webkit2gtk-4.0; this points it at 4.1)"

.PHONY: help install linux-deps fetch verify qa go-test phpstan dresscode dresscode-hook golangci oxc security check check-app e2e e2e-visual e2e-packaged i18n i18n-check mcp-bridge-data bench build run dev editor debug demo down destroy kill-orphans bundle zip dist tarball windows deb logs serve clean checksums

.DEFAULT_GOAL := help
Expand Down Expand Up @@ -49,18 +68,17 @@ endif
$(MAKE) fetch

# The one build dependency mise cannot pin: webview is cgo, and on Linux that means the
# system GTK-3 and WebKit2GTK dev headers — distro packages, not a toolchain. webview_go
# also hardcodes `pkg-config webkit2gtk-4.0`, which Ubuntu 24.04 dropped for 4.1 (same API
# over libsoup3), so a shim .pc that just requires 4.1 keeps the build on a current distro.
# Debian/Ubuntu (apt) is the only Linux supported right now — and the path CI and the .deb
# build on — so gate on apt up front and fail clearly on Arch/Fedora/others rather than
# half-installing. The pkg-config guard then makes a re-run a no-op that never re-sudos.
linux-deps: ## Install the Linux webview build deps (GTK/WebKit dev headers, Debian/Ubuntu only)
@command -v apt-get >/dev/null 2>&1 || { echo "linux-deps: Debian/Ubuntu (apt) is the only supported Linux distribution right now. On Arch, Fedora and others, install the GTK-3 and WebKit2GTK 4.1 dev headers with your own package manager."; exit 1; }
# system GTK-3 and WebKit2GTK dev headers — distro packages, not a toolchain. Only the
# install is distro-specific, so the headers being there already is the first thing asked:
# that answer is the same everywhere, and a distro we have no package names for (Arch,
# Fedora) is then a checkout that builds rather than one that is turned away. apt is the
# install path CI and the .deb build on; anywhere else names what to install and stops,
# which beats half-installing. The pkg-config guard makes a re-run a no-op that never
# re-sudos. $(WEBKIT_SHIM) is the other half, and needs no package manager at all.
linux-deps: $(WEBKIT_SHIM) ## Install the Linux webview build deps (GTK/WebKit dev headers)
@pkg-config --exists gtk+-3.0 webkit2gtk-4.1 2>/dev/null && echo "gtk/webkit dev headers present" || { \
command -v apt-get >/dev/null 2>&1 || { echo "linux-deps: install the GTK-3 and WebKit2GTK 4.1 dev headers with your package manager (Arch: gtk3 webkit2gtk-4.1; Fedora: gtk3-devel webkit2gtk4.1-devel), then re-run."; exit 1; }; \
sudo apt-get update && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev; }
@pkg-config --exists webkit2gtk-4.0 2>/dev/null || printf 'Name: webkit2gtk-4.0\nDescription: Shim onto webkit2gtk-4.1\nVersion: 2.44.0\nRequires: webkit2gtk-4.1\n' \
| sudo tee /usr/lib/$(UNAME_M)-linux-gnu/pkgconfig/webkit2gtk-4.0.pc >/dev/null

fetch: app/adminer.php app/editor.php app/src/Settings/Plugins/available app/src/Settings/Theme/designs/README.md bin/frankenphp$(EXE) ## Download frankenphp, build adminer from its release sources + patches/ (pinned, checksum-verified)

Expand All @@ -83,14 +101,24 @@ app/editor.php: $(ADMINER_BUILD)

# The release zip is the full source tree: adminer.php and editor.php are compiled from it, and
# plugins/ and designs/ copied out of it. Same zip, same pinned tag — plugins can never drift from the core.
.cache/adminer-src.zip:
#
# The version is in the filename, and that is the whole point: with a plain adminer-src.zip a
# checkout that had downloaded one version kept it forever, because make saw the file and called
# it up to date. Bumping ADMINER_VERSION then failed in the extract below ("cannot stat
# .cache/src-tmp/adminer-<new>") on every machine with a warm cache, ours and a contributor's
# alike, and only `rm` fixed it. Now a bump names a file that is not there yet.
ADMINER_ZIP = .cache/adminer-src-$(ADMINER_VERSION).zip

$(ADMINER_ZIP):
@mkdir -p .cache
curl -fsSL --retry 3 --retry-delay 2 -o $@ $(ADMINER_URL)/adminer-$(ADMINER_VERSION).zip
@# The zips of every version we have ever built are no use once this one is here.
@find .cache -maxdepth 1 -name 'adminer-src-*.zip' ! -name $(notdir $@) -delete

# Extracted whole, once. Selecting with a pattern like 'designs/*' is not portable:
# macOS and linux unzip let * match a slash and recurse, the windows one does not, so
# only the file directly inside designs/ came out and every design silently vanished.
.cache/adminer-src: .cache/adminer-src.zip
.cache/adminer-src: $(ADMINER_ZIP)
rm -rf $@ .cache/src-tmp
unzip -qo $< -d .cache/src-tmp
mv .cache/src-tmp/adminer-$(ADMINER_VERSION) $@
Expand Down Expand Up @@ -246,7 +274,7 @@ dresscode-hook:
# A cache per checkout. The default one is per user and keeps absolute paths, so an agent's
# worktree linted second got the first one's findings at that worktree's paths; once it was
# deleted, the //nolint comments there could not be read and the lint failed on them.
golangci: mcp-bridge-data # the bridge embeds it: go cannot even type-check the package without it
golangci: mcp-bridge-data $(WEBKIT_SHIM) # the bridge embeds it: go cannot even type-check the package without it
GOLANGCI_LINT_CACHE=$(CURDIR)/.cache/golangci-lint go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_VERSION) run ./...

# Format-check CSS and JS with oxfmt and lint JS with oxlint; the file list is package.json's.
Expand Down Expand Up @@ -303,7 +331,7 @@ security:

# Static checks, every one from a tool we already have: the php is the frankenphp we
# download, the rest ship with macOS or the go toolchain. Nothing to install.
qa: fetch app/vendor i18n mcp-bridge-data ## Run every static check (php, go, js lint + formatting)
qa: fetch app/vendor i18n mcp-bridge-data $(WEBKIT_SHIM) ## Run every static check (php, go, js lint + formatting)
./bin/frankenphp$(EXE) php-cli cli/lint.php
@# No database and no browser: it replays adminer's own parser over a dump.
./bin/frankenphp$(EXE) php-cli tests/postgres/copy-import/run.php
Expand Down Expand Up @@ -338,6 +366,8 @@ qa: fetch app/vendor i18n mcp-bridge-data ## Run every static check (php, go, j
./bin/frankenphp$(EXE) php-cli tests/shortcuts/ShortcutsTest.php
./bin/frankenphp$(EXE) php-cli tests/cli/WindowsIcoTest.php
./bin/frankenphp$(EXE) php-cli tests/cli/I18nTest.php
./bin/frankenphp$(EXE) php-cli tests/cli/LinuxDistroTest.php
./bin/frankenphp$(EXE) php-cli tests/cli/OmarchyThemeTest.php
@sh tests/cli/pick-os-test.sh
@sh tests/cli/kill-orphans-test.sh
@sh tests/cli/dresscode-test.sh
Expand Down Expand Up @@ -433,7 +463,7 @@ build.noindex/logo.ico: assets/logo.png app/cli/Command/WindowsIco.php app/vendo
@mkdir -p $(dir $@)
$(if $(EXE),PHP_INI_SCAN_DIR=$(WIN_INI)) $(CLI) windows-ico

build: fetch i18n mcp-bridge-data $(WIN_ICON) ## Build the launcher binary
build: fetch i18n mcp-bridge-data $(WIN_ICON) $(WEBKIT_SHIM) ## Build the launcher binary
go build -ldflags "$(LDFLAGS)" -o build.noindex/adminer-desktop$(EXE) ./launcher
# NSLocalizedString reads the .lproj folders beside an unbundled binary as it does a bundle's
# Resources: without them `make run` showed the menu's keys (menu.edit) for its titles.
Expand Down Expand Up @@ -461,7 +491,7 @@ editor: build ## Run in editor mode
# docker skips it rather than refusing to debug.
debug: build ## Run with Safari's Web Inspector attached, and a seeded mysql (docker)
@os=$$(./cli/pick-os.sh); \
./cli/demo-mysql.sh $(DEMO_MYSQL) || echo "no mysql: docker is not running"; \
./cli/demo-mysql.sh $(DEMO_MYSQL) || true; \
ADMINER_DESKTOP_SIMULATE_OS=$$os ./build.noindex/adminer-desktop$(EXE) -debug

# The app in dev mode against seeded demo data, for clicking around by hand. Brings up
Expand Down
9 changes: 9 additions & 0 deletions app/AdminerDesktop.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@
use Desktop\I18n\Locale;
use Desktop\Import;
use Desktop\Latte;
use Desktop\LinuxDistro;
use Desktop\Mcp;
use Desktop\Os;
use Desktop\SettingKey;
use Desktop\Settings\Dialog;
use Desktop\Settings\Plugins\PluginList;
use Desktop\Settings\Theme\OmarchyTheme;
use Desktop\Settings\Theme\Theme;
use Desktop\Shortcuts\Shortcuts;
use Desktop\UserSettings;
Expand Down Expand Up @@ -243,6 +245,10 @@ function head(): ?string {
if ($color = $this->connections->current()?->color()) {
$css .= '--ad-conn:' . $color . ';';
}
if (LinuxDistro::look() === LinuxDistro::Omarchy) {
$appearance = $this->userSettings->get(SettingKey::Appearance, 'auto');
$css .= OmarchyTheme::current(is_string($appearance) ? $appearance : 'auto');
}
if ($css !== '') {
echo "<style>:root{" . $css . "}</style>\n";
}
Expand Down Expand Up @@ -311,6 +317,9 @@ function csp(array &$csp): void {
*/
function bodyClass(): void {
echo " " . Os::look()->bodyClass();
if ($distro = LinuxDistro::look()) {
echo " " . $distro->bodyClass();
}
// The launcher sets this under -debug; the desktop scripts read it to stand down so
// the web inspector's own behaviour is unobstructed.
if (Env::Debug->get()) {
Expand Down
4 changes: 3 additions & 1 deletion app/src/Env.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ enum Env: string {
case DataDir = 'ADMINER_DESKTOP_DATA';
/** Set under -debug: turns on Tracy and the web inspector's own affordances. */
case Debug = 'ADMINER_DESKTOP_DEBUG';
/** Which OS the UI dresses as under -debug, an Os case (Os::look). Ignored without -debug. */
/** Which OS the UI dresses as under -debug, an Os or LinuxDistro case. Ignored without -debug. */
case SimulateOs = 'ADMINER_DESKTOP_SIMULATE_OS';
/** The desktop session's own, not the launcher's: it only passes through (LinuxDistro::current). */
case CurrentDesktop = 'XDG_CURRENT_DESKTOP';
/** `make demo` only: the throwaway connection demo-login.js fills and submits. */
case Demo = 'ADMINER_DESKTOP_DEMO';
/** The launcher's own path (Go's os.Executable), so the MCP panel can print the exact
Expand Down
61 changes: 61 additions & 0 deletions app/src/LinuxDistro.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);

namespace Desktop;

/** The Linux flavours with a look of their own. A flavour is a distribution and its desktop together,
* because the distribution alone cannot tell them apart: Kubuntu reports the ID of Ubuntu, and only
* its desktop differs. Anything else has no case, and keeps the plain Linux look.
*/
enum LinuxDistro: string {
case Omarchy = 'omarchy';
case Ubuntu = 'ubuntu';
case Kubuntu = 'kubuntu';
case Mint = 'mint';

/** The flavour the UI dresses as: this machine's, unless -debug asks for another OS or flavour. */
static function look(): ?self {
$simulated = Env::Debug->get() ? (string) Env::SimulateOs->get() : '';
if ($simulated !== '') {
return self::tryFrom($simulated);
}
return Os::current() === Os::Linux ? self::current() : null;
}

/** This machine's flavour. */
static function current(): ?self {
$osRelease = @file_get_contents('/etc/os-release') ?: @file_get_contents('/usr/lib/os-release') ?: '';
$home = getenv('HOME');
// omarchy-theme-set keeps the current theme under this state folder and the user's under .config.
$omarchy = $home !== false && (is_dir("$home/.local/state/omarchy") || is_dir("$home/.config/omarchy"));
return self::detect($osRelease, (string) Env::CurrentDesktop->get(), $omarchy);
}

/** Omarchy first, as it installs onto Arch and leaves the ID alone; only in its Hyprland session,
* since another session on that machine does not look like it.
* @param string $osRelease the contents of os-release
* @param string $desktop XDG_CURRENT_DESKTOP, a colon-separated list such as "ubuntu:GNOME"
*/
static function detect(string $osRelease, string $desktop, bool $omarchyInstalled): ?self {
$desktops = explode(':', strtolower($desktop));
if ($omarchyInstalled && in_array('hyprland', $desktops, true)) {
return self::Omarchy;
}
preg_match('~^ID="?([^"\n]+)~m', $osRelease, $match);
return match ($match[1] ?? '') {
'linuxmint' => self::Mint,
// kubuntu too: no source shows what Kubuntu's os-release says, and KDE decides either way.
'ubuntu', 'kubuntu' => match (true) {
in_array('kde', $desktops, true) => self::Kubuntu,
in_array('gnome', $desktops, true) => self::Ubuntu,
default => null,
},
default => null,
};
}

/** The class on <body>, and the name of the stylesheet Theme::cssMap() loads if there is one. */
function bodyClass(): string {
return $this->value . '-linux';
}
}
8 changes: 6 additions & 2 deletions app/src/Os.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ static function current(): self {
/** The OS the UI dresses as: the real one, unless -debug asks for another (Env::SimulateOs).
* Only for how the page looks. Paths and printed commands stay on current(). */
static function look(): self {
$simulated = Env::Debug->get() ? self::tryFrom((string) Env::SimulateOs->get()) : null;
return $simulated ?? self::current();
if (!Env::Debug->get()) {
return self::current();
}
$simulated = (string) Env::SimulateOs->get();
// A Linux flavour dresses as Linux first (LinuxDistro::look).
return self::tryFrom($simulated) ?? (LinuxDistro::tryFrom($simulated) ? self::Linux : self::current());
}

/** The class on <body> the theme keys off (AdminerDesktop::bodyClass). */
Expand Down
Loading
Loading