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)
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
//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)
|
||||
}
|
||||
Reference in New Issue
Block a user