From 3c8cd32ebbc2dbd9282f7f58a9534e01ddbc86ec Mon Sep 17 00:00:00 2001 From: George Dumitrescu Date: Thu, 23 Jul 2026 10:54:17 +0300 Subject: [PATCH 1/2] fix(ui): open the terminal in the site path on ptyxis The open-terminal button on a site launched ptyxis with a bare --working-directory flag. ptyxis is single-instance and applies that flag only when it is paired with --new-window, --tab or -x, so a plain launch ignored the directory and raised a new window in the home directory instead of the project. It reached anyone whose terminal resolved to ptyxis, the default on Fedora and on Ubuntu installs that carry it, while one-window-per-process emulators were unaffected. The button passes --new-window alongside --working-directory now, so ptyxis opens the new window in the site or worktree path. Closes #1103 --- internal/ui/server.go | 57 ++++++++++++++++++-------------- internal/ui/terminal_env_test.go | 24 ++++++++++++++ 2 files changed, 57 insertions(+), 24 deletions(-) diff --git a/internal/ui/server.go b/internal/ui/server.go index 28572f6b7..81b82d57f 100644 --- a/internal/ui/server.go +++ b/internal/ui/server.go @@ -499,33 +499,36 @@ func graphicalEnv() []string { return out } -// openTerminalAt opens the user's preferred terminal emulator in dir. -// It checks $TERMINAL first, then falls back to a list of common emulators. -func openTerminalAt(dir string) error { - type termCmd struct { - bin string - args []string - } +// terminalCmd is a terminal emulator binary and the args that open it at a dir. +type terminalCmd struct { + bin string + args []string +} - candidates := []termCmd{} +// terminalDirCandidates returns the ordered emulator candidates for opening a +// new terminal at dir. $TERMINAL leads, then a fixed fallback list. +func terminalDirCandidates(dir string) []terminalCmd { + candidates := []terminalCmd{} if t := os.Getenv("TERMINAL"); t != "" { - candidates = append(candidates, termCmd{t, []string{}}) + candidates = append(candidates, terminalCmd{t, []string{}}) } candidates = append(candidates, - termCmd{"kitty", []string{"--directory", dir}}, - termCmd{"foot", []string{"--working-directory", dir}}, - termCmd{"alacritty", []string{"--working-directory", dir}}, - termCmd{"wezterm", []string{"start", "--cwd", dir}}, - termCmd{"ghostty", []string{"--working-directory=" + dir}}, - termCmd{"ptyxis", []string{"--working-directory", dir}}, - termCmd{"konsole", []string{"--separate", "--workdir", dir}}, - termCmd{"gnome-terminal", []string{"--working-directory", dir}}, - termCmd{"xfce4-terminal", []string{"--working-directory", dir}}, - termCmd{"tilix", []string{"--working-directory", dir}}, - termCmd{"terminator", []string{"--working-directory", dir}}, - termCmd{"xterm", []string{"-e", "sh", "-c", `cd "$0" && exec "$SHELL"`, dir}}, + terminalCmd{"kitty", []string{"--directory", dir}}, + terminalCmd{"foot", []string{"--working-directory", dir}}, + terminalCmd{"alacritty", []string{"--working-directory", dir}}, + terminalCmd{"wezterm", []string{"start", "--cwd", dir}}, + terminalCmd{"ghostty", []string{"--working-directory=" + dir}}, + // ptyxis is single-instance: --working-directory is honoured only + // alongside --new-window/--tab/-x, so a bare launch lands in $HOME. + terminalCmd{"ptyxis", []string{"--new-window", "--working-directory", dir}}, + terminalCmd{"konsole", []string{"--separate", "--workdir", dir}}, + terminalCmd{"gnome-terminal", []string{"--working-directory", dir}}, + terminalCmd{"xfce4-terminal", []string{"--working-directory", dir}}, + terminalCmd{"tilix", []string{"--working-directory", dir}}, + terminalCmd{"terminator", []string{"--working-directory", dir}}, + terminalCmd{"xterm", []string{"-e", "sh", "-c", `cd "$0" && exec "$SHELL"`, dir}}, ) if runtime.GOOS == "darwin" { @@ -533,12 +536,18 @@ func openTerminalAt(dir string) error { // command — cleaner than `do script "cd ... && exec $SHELL"` which types // the command visibly into the shell. iTerm2 supports the same via open. if _, err := os.Stat("/Applications/iTerm.app"); err == nil { - candidates = append(candidates, termCmd{"open", []string{"-a", "iTerm", dir}}) + candidates = append(candidates, terminalCmd{"open", []string{"-a", "iTerm", dir}}) } - candidates = append(candidates, termCmd{"open", []string{"-a", "Terminal", dir}}) + candidates = append(candidates, terminalCmd{"open", []string{"-a", "Terminal", dir}}) } - for _, t := range candidates { + return candidates +} + +// openTerminalAt opens the user's preferred terminal emulator in dir. +// It checks $TERMINAL first, then falls back to a list of common emulators. +func openTerminalAt(dir string) error { + for _, t := range terminalDirCandidates(dir) { bin, err := exec.LookPath(t.bin) if err != nil { continue diff --git a/internal/ui/terminal_env_test.go b/internal/ui/terminal_env_test.go index 69a8893ec..d702dfc77 100644 --- a/internal/ui/terminal_env_test.go +++ b/internal/ui/terminal_env_test.go @@ -47,6 +47,30 @@ func TestGraphicalEnvPreservesBaseEnvAndPatchesDisplay(t *testing.T) { } } +func TestTerminalDirCandidatesOpenPtyxisInDir(t *testing.T) { + t.Setenv("TERMINAL", "") + const dir = "/home/user/project" + var ptyxis *terminalCmd + for _, c := range terminalDirCandidates(dir) { + if c.bin == "ptyxis" { + cp := c + ptyxis = &cp + } + } + if ptyxis == nil { + t.Fatal("ptyxis is not among the terminal candidates") + } + joined := strings.Join(ptyxis.args, " ") + // ptyxis is single-instance: without --new-window (or --tab/-x) it ignores + // --working-directory and opens a new window in $HOME instead of the site. + if !strings.Contains(joined, "--new-window") { + t.Errorf("ptyxis args %v lack --new-window, so %q would be ignored", ptyxis.args, dir) + } + if !strings.Contains(joined, dir) { + t.Errorf("ptyxis args %v do not carry the target dir %q", ptyxis.args, dir) + } +} + func TestGraphicalEnvDoesNotDuplicateKeys(t *testing.T) { t.Setenv("XDG_SESSION_TYPE", "wayland") env := graphicalEnv() From 355a717897ed288669db05b189cccfe5d46f3ab0 Mon Sep 17 00:00:00 2001 From: George Dumitrescu Date: Thu, 23 Jul 2026 10:54:26 +0300 Subject: [PATCH 2/2] release: v1.30.1 This patch carries two fixes on top of 1.30.0. On macOS lerd stopped prompting to reinstall the DNS sudoers rule on every lerd start, where the passwordless liveness probe was hardcoded to a Linux only tool and read its own inevitable refusal as proof the grant was gone. And the web UI open-terminal button opens in the site directory again where ptyxis is the terminal, rather than falling back to the home directory. The version bumps to 1.30.1 and the changelog gains a 1.30.1 section above 1.30.0. --- docs/changelog.md | 11 +++++++++++ internal/version/version.go | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 7c0a94352..82712c227 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -7,6 +7,17 @@ Lerd uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [1.30.1] - 2026-07-23 + +Two fixes: `lerd start` stopped prompting to reinstall the DNS sudoers rule on macOS every run, and the web UI's open-terminal button opens in the site directory again where ptyxis is the terminal. + +### Fixed + +- **macOS prompted to reinstall the DNS sudoers rule on every `lerd start`** (#1101). The passwordless-liveness probe that decides whether the drop-in is still active was hardcoded to `resolvectl`, a systemd tool that exists only on Linux, so on macOS `sudo -n` always refused with "a password is required", which the probe read as a conclusive "the grant is gone" and rewrote the rule, prompting for sudo, even when the installed `/etc/sudoers.d/lerd` was byte identical to its recorded hash. The probe command is platform specific now: Linux keeps `resolvectl --version`, macOS exercises `mkdir -p /etc/resolver`, a command the drop-in already grants verbatim and which is idempotent, so it succeeds silently when the grant is live and only reports it gone when sudo genuinely refuses. +- **The open-terminal button opened in the home directory on ptyxis** (#1103). The web UI launched ptyxis with a bare `--working-directory`, but ptyxis is single-instance and applies that flag only alongside `--new-window`, `--tab` or `-x`, so it was ignored and a new window opened in the home directory instead of the site. The button passes `--new-window` now, so the terminal opens in the site or worktree path. Every other emulator was already correct and is unchanged. + +--- + ## [1.30.0] - 2026-07-22 The 1.30.0 line shrinks what a PHP version owns and widens what the dashboard can reach. Custom extensions and packages became one declared set applied to every image lerd builds, and php.ini follows with a shared scope that fills the gaps under each version's own file, so a site that changes version stops silently losing what the developer asked for. The dashboard manages an engine's databases from its service page with snapshots, export and import, offers to install a service the project asks for but never had, keeps every notification in a bell that survives a reload, renders log output in colour and hands any log to a terminal, and groups the services list by type. `lerd doctor` stops merely reporting and learns to repair what it safely can, notifications grow a native desktop sink alongside the new Lerd desktop app, and the terminal dashboard moves onto the Charm v2 stack. Git worktrees get a round of their own, resolving their own PHP version, their own image, their own pin and their own database from wherever a command is run. Underneath all of it there is a long tail of resolution and resource work: `.test` keeps answering with no network at all and comes back after a sleep, nginx stops rejecting a config over a directive lerd also declares, and the daemon stops paying for background work while nobody is looking at it. diff --git a/internal/version/version.go b/internal/version/version.go index 5a3597568..40208d448 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -9,7 +9,7 @@ import "fmt" // -X github.com/geodro/lerd/internal/version.Commit= // -X github.com/geodro/lerd/internal/version.Date= var ( - Version = "1.30.0" + Version = "1.30.1" Commit = "none" Date = "unknown" )