Files
agent-lib/internal/notify/toast_windows.go
T
m3ta-chiron 949a431c4c feat: status command + windows toast notifications
- status: read-only report of deployed/repository revision, pending changes,
  skipped items (with reasons), unrecognized local items, binary version;
  --json machine-readable; exit 1 when local modifications block repository
  updates (headless drift checks)
- sync/status share a read-only prepare() front half
- notify package: windows toast via PowerShell script (go-toast mechanism,
  zero deps, build-tag gated), noop on linux/macos; cross-compile verified
- toast fires exactly once per sync with warnings, never for routine syncs;
  notification failures are logged and never fail the sync (fail-open)
2026-08-23 10:40:25 +02:00

50 lines
1.7 KiB
Go

//go:build windows
package notify
import (
"fmt"
"os/exec"
"strings"
)
// PowerShellToast raises a Windows toast notification by invoking
// PowerShell with an AppId-tagged script — the same mechanism go-toast
// uses, without the dependency.
type PowerShellToast struct {
AppID string
}
// Platform returns the PowerShell toast sender on Windows.
func Platform() Sender { return PowerShellToast{AppID: "agent-lib"} }
var toastScript = `$app = '%[1]s'
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
$tmpl = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)
$txt = $tmpl.GetElementsByTagName('text')
$txt.Item(0).AppendChild($tmpl.CreateTextNode('%[2]s')) | Out-Null
$txt.Item(1).AppendChild($tmpl.CreateTextNode('%[3]s')) | Out-Null
$toast = [Windows.UI.Notifications.ToastNotification]::new($tmpl)
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($app).Show($toast)`
// Send fires one toast. It requires an interactive user session; callers
// must treat errors as non-fatal.
func (p PowerShellToast) Send(title, body string) error {
appID := p.AppID
if appID == "" {
appID = "agent-lib"
}
script := fmt.Sprintf(toastScript, escape(appID), escape(title), escape(body))
cmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", script)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("toast failed: %v: %s", err, strings.TrimSpace(string(out)))
}
return nil
}
// escape makes s safe inside a PowerShell single-quoted string.
func escape(s string) string {
r := strings.NewReplacer("'", "''", "\n", " ", "\r", " ")
return r.Replace(s)
}