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
11 changes: 11 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
57 changes: 33 additions & 24 deletions internal/ui/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -499,46 +499,55 @@ 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" {
// `open -a Terminal dir` opens a new window at dir without echoing any
// 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
Expand Down
24 changes: 24 additions & 0 deletions internal/ui/terminal_env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion internal/version/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import "fmt"
// -X github.com/geodro/lerd/internal/version.Commit=<sha>
// -X github.com/geodro/lerd/internal/version.Date=<iso8601>
var (
Version = "1.30.0"
Version = "1.30.1"
Commit = "none"
Date = "unknown"
)
Expand Down
Loading