Repository navigation
Releases: mario/drey
Release list
drey 0.1.9
Fixed
- The daemon's socket, lock and log lived under
dirs::cache_dir()
(~/Library/Caches/dreyon macOS) when neitherDREY_RUNTIME_DIRnor
XDG_RUNTIME_DIRwas set. That directory is fair game for the OS and for
cache-cleaning tools to prune at any time, including while the daemon is
still running: oncedaemon.sockis gone, the daemon keeps listening on a
path nothing can reach, and the next client, finding no socket there,
starts a second daemon rather than reusing the first. Repeated over days
this produced a pile of idle, unreachabledrey daemonprocesses that
never exited on their own. The fallback now prefers the system temp
directory ($TMPDIRon macOS) over the cache directory, and the daemon
also checks every 60 seconds that its socket path still points at its own
listener, exiting if it doesn't, so an orphaned daemon cleans itself up
instead of accumulating indefinitely.
Full Changelog: v0.1.8...v0.1.9
drey 0.1.8
Fixed
servenow gives up on a client that never writes anything, instead of
parking on stdin forever. The daemon side already bounded the reverse
handshake (0.1.5'sINIT_TIMEOUT: a backend that doesn't answer
initializewithin 60 seconds is given up on), but nothing bounded a
client that opens the shim's stdin and never sends its owninitialize.
That left an orphaneddrey serveprocess per abandoned attempt, sitting
outsidedrey status(which lists backends, not shims) and outside process
accounting until something went looking. A client that writes nothing
within 60 seconds now gets the same treatment: the shim exits instead of
hanging.
Full Changelog: v0.1.7...v0.1.8
drey 0.1.7
Fixed
- The guard against a server config pointing back at drey itself now checks
the exact executable path instead of banning its whole parent directory.
Homebrew installsdreyat/usr/local/bin/drey, the same directory as
asdfand every other server binary it manages;drey installcorrectly
points asdf-managed servers at/usr/local/bin/asdf
(command = "/usr/local/bin/asdf",args = ["exec", "rust-analyzer"]), but
the guard read that as drey pointing at itself and refused to spawn it. Six
of the twelve built-in servers route through asdf, so this hung silently for
anyone with drey installed next to a version manager in the same directory,
the same "attached to nothing, no error" symptom as 0.1.5's shim loop.
Full Changelog: v0.1.6...v0.1.7
drey 0.1.6
Fixed
- Wrapper scripts written by
drey installnow keep the path they were
invoked through instead of resolving it. Homebrew runsdreyvia
/usr/local/bin/drey, a symlink it repoints at every upgrade to point at
the newly installed version's Cellar path; canonicalizing that symlink baked
the old, versioned Cellar path into every wrapper. The next upgrade deleted
that path, so every wrapper drey had installed, not just rust-analyzer's,
called a binary that no longer existed, and the daemon thatserveis
supposed to autostart never got to run.
Full Changelog: v0.1.5...v0.1.6
drey 0.1.5
Fixed
-
The shim directory of the version manager drey launches through is now
removed from the server's environment, alongside drey's own. Launching
rust-analyzer asasdf exec rust-analyzerreaches a rustup proxy inside the
asdf installation; when that toolchain has norust-analyzercomponent, the
proxy falls back to the nextrust-analyzeronPATH, which is asdf's shim,
which execsasdf exec rust-analyzeragain. The two proxies handed the name
back and forth and never reached a language server. What you saw was a client
attached to nothing: no diagnostics, no completion, no error, and adrey serveprocess that sat there for days. With the shims gone from the child's
PATH, rustup says which component is missing instead. -
A server that does not answer
initializewithin 60 seconds is now given up
on, with its stderr attached. Every real server replies in well under a
second and indexes afterwards, so a slow handshake is a wedged one. There was
no bound before, so a single stuck spawn kept every client on that workspace
waiting for as long as the daemon lived.
Full Changelog: v0.1.4...v0.1.5
drey 0.1.4
Fixed
-
Servers managed by asdf are launched through
asdf execagain, instead of
the pathasdf whichreports. That path is often not a program: for
asdf-managed Rust it is a rustup proxy that needsRUSTUP_HOMEpointing
inside the asdf installation, and onlyasdf execsets it. Unwrapped, the
proxy searched~/.rustup, found no default toolchain there, and exited
during initialize, so every Rust client silently had no language server at
all. gopls, ruff, pyright and ruby-lsp resolve through the same shims and
were affected wherever their runtime needed the environment too.Unwrapping was meant to stop every workspace from pinning to whatever
version asdf picked at daemon start. Recordingasdf execfixes that
properly rather than sidestepping it: the daemon already spawns servers with
the workspace root as the working directory, which is what asdf dispatches
on, so each workspace now gets the version its.tool-versionsasks for. -
A server that dies during initialize now reports what it printed on stderr.
That output went to the log at debug level, so the usual causes (a missing
toolchain, an unknown flag, a broken shim) producedexited during initializeand nothing else. Diagnosing the bug above meant turning up
DREY_LOGand reproducing it; the message now carries the reason.
Full Changelog: v0.1.3...v0.1.4
drey 0.1.3
Fixed
Content-Lengthis matched without regard to case, as header names are. Two
spellings were accepted andCONTENT-LENGTH:was not, so a client using it
got "LSP message without Content-Length header", a confusing way to report
unexpected capitalisation. A field whose name merely ends incontent-length
is still ignored rather than parsed as the length.- The test suites leaked daemons. A daemon detaches and only
drey stopends
it, and the CLI suite never stopped what it started, so 8 survived every run.
Dozens accumulated here over a day, the oldest at 18 hours, and the suite
began timing out under the process pressure. The real-server smoke test
leaked a daemon and a rust-analyzer too. Both now clean up.
Full Changelog: v0.1.2...v0.1.3
drey 0.1.2
Fixed
- Root widening could climb to
$HOME. A strayCargo.toml,package.jsonor
go.modin your home directory merged every project underneath it into one
backend indexing the entire home directory. The walk now stops below home in
both directions: it will not adopt home as a root, and a client that opened
$HOMEitself will not climb into/Usersor/. - A duplicate
Content-Lengthheader let the last value win, which
desynchronised the stream: the sender and drey then disagreed about where the
message ended, and every message after it was read at the wrong offset with no
error to point at the cause. It is now refused, naming both values.
Changed
- Hitting the 64-entry cap on server-initiated requests held for an absent
client now logs at warn rather than passing silently. A server whose requests
are being refused is wedged or misconfigured, and the log was the only place
that would have shown it.
Note for anyone upgrading
If you had a marker file directly in $HOME, projects that previously collapsed
into a single backend now get one each. That is more memory and more indexing on
first use, and it is the correct behaviour: those projects were never one
workspace. Nothing else changes, and no configuration is affected.
Full Changelog: v0.1.1...v0.1.2
drey 0.1.1
Added
drey installanddrey uninstall, so a binary from Homebrew or
cargo installcan set up thePATHinterposition without cloning the
repository and rebuilding. The wrappers exec whichever drey binary you ran
installfrom, rather than assuming~/.drey/bin/drey. Both take
--dry-run;installtakes--forceto replace files in~/.drey/binthat
drey did not write.- Apache License 2.0, replacing the proprietary licence.
CONTRIBUTING.md,CODE_OF_CONDUCT.md,SECURITY.md, and aNOTICEfile.docs/architecture.mdexplaining the three ideas the design rests on, and
docs/releasing.mdcovering crates.io and the Homebrew tap.- CI on Linux and macOS: fmt, clippy with warnings as errors, unit tests, the
Python end-to-end suite, an MSRV 1.85 check, andcargo denyfor licences and
advisories. - Release workflow publishing to crates.io from a tag push, via trusted
publishing rather than a stored token. - Property tests (proptest) over frame round-trips, UTF-16 position maths, and
incremental edit application against a reference implementation.
Changed
scripts/install.shandscripts/uninstall.share thin wrappers now: they
build or remove the binary and delegate the rest to the new subcommands. The
discovery, config, wrapper andPATHlogic is no longer duplicated in shell.drey uninstallleaves~/.config/drey/config.tomlin place and says so,
where the old script deleted it. It also removes only files carrying drey's
marker comment from~/.drey/bin.
Full Changelog: v0.1.0...v0.1.1
drey 0.1.0
One language server per workspace, shared by every editor and agent.
Every LSP client starts its own language server. Two Claude Code sessions on one
Rust workspace means two rust-analyzer processes, each holding a complete salsa
database, and 7.4 GB of memory to answer questions about one copy of one
codebase. drey makes them share.
The same applies to gopls, tsserver, clangd, pyright and jdtls. Rust is the
example because it is the one with the measurement.
Install
cargo install drey
# or
brew install mario/drey/dreyThen ./scripts/install.sh puts wrapper scripts on your PATH, so existing
editor and agent configs pick up drey without being changed.
What it does
The shim looks exactly like a language server on stdio. The daemon owns one real
server per workspace and autostarts on first use.
Clients attach by root containment rather than equality, so opening a single
crate inside a Cargo workspace costs nothing: it joins the server that already
indexed it. Git worktrees and branches deliberately stay separate, because
merging them would produce wrong answers, which is worse than using memory.
Two clients with different unsaved edits to the same file still share one
process. Each document keeps a base text and a loaded text, and the daemon swaps
the loaded one rather than forking a second server. Verified against a real
rust-analyzer: with one client holding pub size: u32 and another holding
u64, the same process reports size = 4, align = 0x4 to the first and
size = 8, align = 0x8 to the second. That is a type layout recomputed per
client, not an echo of the text they sent.
Server-initiated requests (workspace/configuration,
client/registerCapability, workspace/applyEdit) are answered by one attached
client on behalf of all. A proxy that drops those silently runs rust-analyzer on
defaults, which is the opposite of what you want when you adopted a proxy to
control memory.
Builtins
rust-analyzer, gopls, typescript, pyright, ruff, clangd, zls, lua, elixir,
jdtls. Anything in ~/.config/drey/config.toml overrides them.
Testing
150 unit tests, 11 CLI integration tests, an end-to-end suite driving the real
binary over stdio against a mock server, and a smoke test against a real
rust-analyzer. CI runs fmt, clippy with warnings as errors, tests on Linux and
macOS, an MSRV 1.85 check, and cargo-deny for licences and advisories.
Status
First release. The daemon has been running my own sessions for a while, but it
has not been through anyone else's setup yet, so expect rough edges and please
report them. Sharing policy is the part where a bug is silent, so bug reports
about clients sharing when they should not are the most valuable kind.