Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2312048
fix(service): repair the macOS launch agent with bootout and bootstrap
vishr Nov 2, 2026
2ebba34
fix(release): remove the launch agent when the cask is uninstalled
vishr Nov 2, 2026
7225930
perf(desktop): ask launchctl for the service status every 30 seconds
vishr Nov 2, 2026
35fbea5
fix(mcp): annotate Chat action tools and correct MCP and service docs
vishr Nov 2, 2026
d34eabc
feat(config)!: give development builds their own data directory
vishr Nov 2, 2026
0294259
fix(release): mark only tagged releases as release builds
vishr Nov 2, 2026
56bced9
fix(command): deny every Driga variant's state to agent commands
vishr Nov 2, 2026
fc0064c
fix(command): resolve sandbox paths component by component
vishr Nov 2, 2026
4b6bb5c
fix(command): root Windows symlink targets the way Windows does
vishr Nov 2, 2026
c2606f0
fix(service): replace the macOS launch agent without racing launchd
vishr Nov 2, 2026
3925627
fix(service): ask the service manager again after a service action
vishr Nov 2, 2026
36500fb
fix(service): keep the recorded environment unless --refresh-env
vishr Nov 2, 2026
bdf14c3
fix(service): refuse to install without drigad and document building it
vishr Nov 2, 2026
064bec0
fix(command): refuse paths through Windows link targets it cannot model
vishr Nov 2, 2026
0ace092
docs: regenerate the CLI reference for service install --refresh-env
vishr Nov 2, 2026
e5c5e46
fix(service): find Driga.app by bundle identifier, not by name
vishr Nov 2, 2026
8a68cac
fix(spool): keep existing spool directories and files owner-only
vishr Nov 2, 2026
8af7821
style(cli): gofmt the service install command
vishr Nov 2, 2026
b2946c8
fix(release): restore the launch agent only within the upgrading brew…
vishr Nov 2, 2026
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
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,10 @@
# DRIGA_CONFIG: path; default=$XDG_CONFIG_HOME/driga/config.toml or ~/.config/driga/config.toml; optional. Configuration file override.
# DRIGA_CONFIG=

# DRIGA_DATA_DIR: path; default=Linux: $XDG_DATA_HOME/driga or ~/.local/share/driga; macOS: ~/Library/Application Support/driga; optional. Database and fleet data directory override.
# DRIGA_DATA_DIR: path; default=Linux: $XDG_DATA_HOME/driga or ~/.local/share/driga; macOS: ~/Library/Application Support/driga; development builds use driga-dev; optional. Database and fleet data directory override.
# DRIGA_DATA_DIR=

# DRIGA_SPOOL_DIR: path; default=Linux: $XDG_STATE_HOME/driga/events or ~/.local/state/driga/events; macOS: ~/Library/Application Support/driga/events; optional. Hook/ack spool directory override.
# DRIGA_SPOOL_DIR: path; default=Linux: $XDG_STATE_HOME/driga/events or ~/.local/state/driga/events; macOS: ~/Library/Application Support/driga/events; development builds use driga-dev; optional. Hook/ack spool directory override.
# DRIGA_SPOOL_DIR=

# DRIGA_CHAT_SOCKET: path; default=unset; optional. Chat broker socket for the MCP child of one Chat turn.
Expand Down
70 changes: 57 additions & 13 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,15 @@ builds:
- arm64
flags:
- -trimpath
# Only a tagged release is a release build; a snapshot is a development
# build, with its own service label and data directory.
ldflags:
- >-
-s -w
-X main.version={{ .Version }}
-X main.commit={{ .FullCommit }}
-X main.date={{ .CommitDate }}
-X github.com/labstack/driga/internal/variant.release={{ if not .IsSnapshot }}true{{ end }}

hooks:
post:
Expand Down Expand Up @@ -63,6 +66,7 @@ builds:
-X main.version={{ .Version }}
-X main.commit={{ .FullCommit }}
-X main.date={{ .CommitDate }}
-X github.com/labstack/driga/internal/variant.release={{ if not .IsSnapshot }}true{{ end }}

# Driga.exe, the Windows desktop app: a GUI-subsystem binary that installs
# the Driga service and shows its data.
Expand All @@ -84,6 +88,7 @@ builds:
-X main.version={{ .Version }}
-X main.commit={{ .FullCommit }}
-X main.date={{ .CommitDate }}
-X github.com/labstack/driga/internal/variant.release={{ if not .IsSnapshot }}true{{ end }}

archives:
- id: driga
Expand Down Expand Up @@ -229,29 +234,68 @@ homebrew_casks:
# `app` (DMG) integration.
custom_block: |
app "Driga.app"
require "json"
require "fileutils"
# Uninstalling Driga.app removes the Driga service's launch agent
# with it. An upgrade (or reinstall) runs the old version's uninstall
# in the same brew process as the new version's postflight, so the
# preflight keeps a copy of the plist named for this process; only
# that process's postflight restores it. The copy is deleted when brew
# exits, so an ordinary uninstall leaves none behind, however long a
# slow upgrade takes.
uninstall_preflight do
plist = File.expand_path("~/Library/LaunchAgents/run.driga.service.plist")
if File.exist?(plist)
kept = File.expand_path("~/Library/Caches/run.driga/run.driga.service.#{Process.pid}.plist")
FileUtils.mkdir_p(File.dirname(kept), mode: 0o700)
FileUtils.rm_f(kept)
FileUtils.cp(plist, kept)
File.chmod(0o600, kept)
at_exit { FileUtils.rm_f(kept) }
end
end
# Homebrew skips signal on upgrade, where the app restarts itself.
uninstall launchctl: "run.driga.service",
signal: [["TERM", "run.driga"]]
postflight do
# Migrate before open -a can activate an old same-bundle drigad.
# Fresh installs still wait for the app's first launch.
if File.exist?(File.expand_path("~/Library/LaunchAgents/run.driga.service.plist"))
previous = system_command "/usr/bin/plutil",
args: ["-convert", "json", "-o", "-", File.expand_path("~/Library/LaunchAgents/run.driga.service.plist")],
must_succeed: true
# system_command shell-escapes env: values yet runs no shell, so a
# value with a space would gain a literal backslash. /usr/bin/env
# takes these assignments verbatim.
environment = JSON.parse(previous.stdout).fetch("EnvironmentVariables", {}).map { |name, value| "#{name}=#{value}" }
system_command "/usr/bin/env",
args: [*environment, "#{appdir}/Driga.app/Contents/MacOS/driga", "service", "install"],
plist = File.expand_path("~/Library/LaunchAgents/run.driga.service.plist")
kept = File.expand_path("~/Library/Caches/run.driga/run.driga.service.#{Process.pid}.plist")
if !File.exist?(plist) && File.exist?(kept)
FileUtils.mkdir_p(File.dirname(plist))
FileUtils.cp(kept, plist)
File.chmod(0o600, plist)
end
# Copies another brew process left behind (one that did not exit
# cleanly) are never restored.
Dir.glob(File.expand_path("~/Library/Caches/run.driga/run.driga.service.*.plist")).each { |path| FileUtils.rm_f(path) }
# The plist keeps the environment it records (the app's login
# shell); install points it at this version and starts it.
if File.exist?(plist)
system_command "#{appdir}/Driga.app/Contents/MacOS/driga",
args: ["service", "install"],
must_succeed: true
end
end
zap trash: [
"~/.config/driga",
"~/Library/Application Support/driga",
"~/Library/Caches/run.driga",
"~/Library/HTTPStorages/run.driga",
"~/Library/LaunchAgents/run.driga.collector.plist",
"~/Library/Logs/driga",
"~/Library/Preferences/run.driga.plist",
"~/Library/Saved Application State/run.driga.savedState",
"~/Library/WebKit/run.driga",
]
caveats: |
Driga.app is in /Applications, and driga links to the CLI inside it.
Open Driga from Finder, Spotlight or with driga app. Its first launch
installs the Driga service (drigad), which collects at login whether or
not the app is open. Upgrades migrate an existing service automatically.
Open Driga once to finish first-launch setup.
not the app is open. Upgrades migrate an existing service automatically,
and brew uninstall removes it. Open Driga once to finish first-launch
setup. Before deleting Driga.app without Homebrew, run
driga service uninstall.
url:
template: "https://github.com/labstack/driga/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
commit_msg_template: "chore: update driga to {{ .Tag }}"
Expand Down
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ just build # builds ./bin/driga, which is not on PATH
just install # deliberately copies it to ~/.local/bin
```

A checkout build is a development build: its Driga service installs as
`run.driga.service.dev` (Linux `driga-dev`, Windows `Driga service (dev)`) and
keeps its database and spool in a `driga-dev` data directory (on macOS
`~/Library/Application Support/driga-dev`), so it never replaces a release's
service or shares its data. The configuration file is shared. Only tagged
GoReleaser releases carry the `internal/variant.release=true` link-time marker;
a `just snapshot` build is a development build too.

### The app's UI

The app's page and the panels MCP App are built from `ui/` with [Bun](https://bun.sh):
Expand Down
28 changes: 20 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Pre-release databases must be reset; `driga doctor` prints the reset command.
| Leftovers | Orphan helper process candidates and local disk cleanup candidates, with evidence and confirmation before removal. |
| Machine health | Memory, CPU, pressure, disk, and agent process attribution. |
| Multiple machines | Optional fleet views over pinned mutual TLS, with explicit pairing and sharing controls. |
| Other interfaces | Versioned JSON, read-only MCP tools, the terminal UI, and opt-in summaries and Chat through Claude. |
| Other interfaces | Versioned JSON, MCP tools (read-only views plus edits to Driga's own dashboards), the terminal UI, and opt-in summaries and Chat through Claude. |

## Install

Expand All @@ -48,8 +48,12 @@ sends notifications under its own name. Hooks keep using the stable Homebrew
`driga` link. After `brew upgrade`, a running app restarts itself on
the new version.

If you installed the Driga service with an earlier release, the app
removes it on its first launch, says so once, and runs the Driga service itself.
The app installs the Driga service as a per-user launch agent on its first
launch and repairs it on every launch, for example after Driga.app moves.
`brew uninstall --cask driga` removes the service with the app, and
`brew uninstall --zap --cask driga` also removes Driga's data, logs and
configuration. Before deleting Driga.app any other way, run
`driga service uninstall`.

On Windows (experimental), use Scoop:

Expand All @@ -71,12 +75,19 @@ checksums, packages, and archive installation.
To build from a source checkout, use the Go version in [go.mod](go.mod) or newer:

```sh
CGO_ENABLED=0 go build ./cmd/driga
./driga version
CGO_ENABLED=0 go build -o bin/driga ./cmd/driga
ln -f bin/driga bin/drigad # the Driga service is the same program as drigad
./bin/driga version
```

Keep Git on PATH and the installed binary at a stable absolute path before
setting up hooks or a service. On a machine without a desktop, install the CLI
`just build` does both. The service runs `drigad` from beside `driga`, so keep
the two together (`driga service install` refuses a `driga` without its
`drigad`), Git on PATH and the installed binaries at a stable absolute path
before setting up hooks or a service. A source build is a development build: its
service installs as `run.driga.service.dev` on macOS, `driga-dev` on Linux and
`Driga service (dev)` on Windows, beside a release's, and keeps its data in a
`driga-dev` data directory. To run a source build as the account's service, build it with
`-ldflags "-X github.com/labstack/driga/internal/variant.release=true"`. On a machine without a desktop, install the CLI
alone and run the Driga service as a service with `driga service install`. CalVer
tags use the year as their major number; Go module major-version rules prevent
`go install ...@<tag>` with this module path. Use a package, archive, or source checkout.
Expand Down Expand Up @@ -147,7 +158,8 @@ driga inbox ack <item-id>
driga cleanup list --sizes
```

`driga mcp` serves the same reads as MCP tools. Use `driga <command> --help`
`driga mcp` serves the same reads as read-only MCP tools, plus tools that
create, edit and delete Driga's own dashboards. Use `driga <command> --help`
for command-specific flags.

## Privacy and what it reads
Expand Down
18 changes: 13 additions & 5 deletions cmd/driga-docgen/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,22 @@ func renderMCP() (string, error) {
return strings.TrimRight(b.String(), "\n") + "\n", nil
}

// toolIndex lists tools with their annotations. MCP's defaults apply to a
// hint a tool leaves out: a tool that is not read-only may be destructive,
// and is not idempotent.
func toolIndex(b *strings.Builder, tools []*sdk.Tool) {
b.WriteString("| Tool | Read-only |\n|---|---|\n")
b.WriteString("| Tool | Read-only | Destructive | Idempotent |\n|---|---|---|---|\n")
yes := map[bool]string{true: "yes", false: "no"}
for _, tool := range tools {
ro := "no"
if tool.Annotations != nil && tool.Annotations.ReadOnlyHint {
ro = "yes"
h := tool.Annotations
if h == nil {
h = &sdk.ToolAnnotations{}
}
fmt.Fprintf(b, "| [%s](#%s) | %s |\n", code(tool.Name), tool.Name, ro)
destructive, idempotent := !h.ReadOnlyHint, h.ReadOnlyHint || h.IdempotentHint
if !h.ReadOnlyHint && h.DestructiveHint != nil {
destructive = *h.DestructiveHint
}
fmt.Fprintf(b, "| [%s](#%s) | %s | %s | %s |\n", code(tool.Name), tool.Name, yes[h.ReadOnlyHint], yes[destructive], yes[idempotent])
}
b.WriteString("\n")
}
Expand Down
61 changes: 48 additions & 13 deletions docs/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,24 +171,54 @@ explains the organization-name fallback and `AssociatedBundleIdentifiers`.
`service install`, which the app runs on launch, points the launch agent at
`Contents/Helpers/Driga Service.app/Contents/MacOS/drigad`, associates
`run.driga.service`, registers both bundles with Launch Services, and atomically
repairs an existing stale plist. It unloads the running service before replacing
its stale definition, then starts it. This migrates the former
`Contents/MacOS/drigad` path even when the version has not changed. Reinstalling
a current definition is harmless. It also removes the `run.driga.collector`
agent earlier releases installed. Development binaries outside the app keep
their ordinary path and have no association; a `driga-dev` build installs as
`run.driga.service.dev`, so it never takes over the release's service.
replaces an existing plist that differs from the one it would write: another
executable, a missing association, an older definition or a refreshed
environment (the app installs with `--refresh-env`; a terminal install keeps
the recorded one). It renders and checks the new plist and writes it beside the
old one first. launchd keeps the definition it bootstrapped, so install then
runs `launchctl bootout gui/<uid>/run.driga.service` whether or not the job's
process runs (a job loaded but not running, as after Driga.app moved, would
otherwise keep its old definition) and polls `launchctl print` until launchd has
let the job go (at most 10 seconds), renames the new plist into place and runs
`launchctl bootstrap gui/<uid>` on it, retrying with backoff while launchd
answers 5, 36 or 37. When launchd refuses the new definition, the previous plist
is restored and bootstrapped again. Start bootstraps an unloaded job, and
`launchctl kickstart` only a loaded job that stays loaded with its process
exited. Without a `gui/<uid>` domain (exit 112), uninstall still deletes the
plist and start and status ask for a desktop login. Driga never uses the deprecated `launchctl load` and
`unload`. This migrates the former `Contents/MacOS/drigad` path even when the
version has not changed. Reinstalling a current definition is harmless. It also
removes the `run.driga.collector` agent earlier releases installed.

The plist keeps the job alive only while its executable exists (`KeepAlive`
`PathState`), with `ProcessType` `Background` and a 30-second
`ThrottleInterval`; the service also exits by itself within a minute of its
executable's deletion. Development binaries outside the app keep their ordinary
path and have no association. Only tagged GoReleaser releases, not snapshots, carry the
`internal/variant.release=true` link-time marker; every other build installs as
`run.driga.service.dev` and collects into a `driga-dev` data directory, so it
never takes over the release's service or shares its data.

The service has its own nested app, `Contents/Helpers/Driga Service.app`, with
bundle identifier `run.driga.service`, `LSBackgroundOnly=true`, matching version
metadata, and Driga's icon and display name. `Contents/Helpers` is for this
launchd-managed helper; `Contents/Library/LoginItems` would imply a Service
Management login item, which this service does not use. The GUI retains
`run.driga`, its existing executable, and the Homebrew `bin/driga` symlink.
The cask postflight runs `service install` when a service plist already exists,
so an upgrade stops the old same-bundle process before the first `open -a`.
It passes the saved launch agent environment through to preserve custom data
paths, configuration, and PATH during that repair.
The cask's `uninstall launchctl: "run.driga.service"` removes the launch agent
and its plist when Driga is uninstalled, and `signal` stops a running app
(Homebrew skips it on upgrade, where the app restarts itself). `zap` also
trashes Driga's data, logs, configuration and caches. Homebrew runs the old
version's uninstall during an upgrade too, in the same brew process as the new
version's postflight, so the cask's `uninstall_preflight` first copies the plist
to `~/Library/Caches/run.driga/run.driga.service.<brew pid>.plist` and deletes
the copy when that process exits. The postflight restores the plist only from
its own process's copy, however long the upgrade took, and runs
`service install` when a service plist exists, so an upgrade (or reinstall)
restores the service and stops the old same-bundle process before the first
`open -a`. The restored plist keeps its recorded environment (custom data
paths, configuration and PATH). An ordinary `brew uninstall` leaves no copy,
and a copy another brew process left behind is deleted, never restored.
Fresh cask installs still wait for the first GUI launch to install the service.
The helper can never register as the GUI's running instance. It also keeps
Prohibited activation policy on the main thread as defense in depth.
Expand Down Expand Up @@ -330,10 +360,15 @@ Retain the archive's license notices.
Use a source checkout and the Go version in `go.mod` or newer:

```sh
CGO_ENABLED=0 go build ./cmd/driga
./driga version
CGO_ENABLED=0 go build -o bin/driga ./cmd/driga
ln -f bin/driga bin/drigad # the Driga service runs drigad, beside driga
./bin/driga version
```

`just build` does both. Keep `drigad` beside `driga`: `driga service install`
refuses to install without it. A source build is a development build, with its
own service label and `driga-dev` data directory.

Check out a release tag first to build that source. Ordinary builds still report
`dev` without linker stamps. CalVer's year is the major number, so Go module
major-version rules prevent `go install ...@<tag>` with this unsuffixed module
Expand Down
Loading
Loading