From 71e3831e64e8ad34bca3e03e389df8a974a7b857 Mon Sep 17 00:00:00 2001 From: Fraser Isbester Date: Thu, 30 Apr 2026 00:26:52 -0700 Subject: [PATCH 1/3] feat: update logo in header to a new style and adjust width --- README.md | 3 +++ internal/tui/app.go | 19 ++++++++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index bd51746..5a94382 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +[![CI](https://github.com/Fraser-Isbester/tusk/actions/workflows/ci.yml/badge.svg)](https://github.com/Fraser-Isbester/tusk/actions/workflows/ci.yml) +[![Go Report Card](https://goreportcard.com/badge/github.com/fraser-isbester/tusk)](https://goreportcard.com/report/github.com/fraser-isbester/tusk) + # Tusk A k9s-style terminal UI for real-time PostgreSQL monitoring and management. Not a database client — a tool for observing and acting on live database activity. diff --git a/internal/tui/app.go b/internal/tui/app.go index 632d738..bf4616a 100644 --- a/internal/tui/app.go +++ b/internal/tui/app.go @@ -375,15 +375,16 @@ func (a *App) updateHeader() { y := "[#FFD700]" r := "[#FF5F5F]" - // TUSK in big open letters (figlet "big" style) - o := "[#D78700]" logo := []string{ - o + tview.Escape(" _______ _ _ _____ _ __") + "[-]", - o + tview.Escape(" |__ __|| | | | / ____|| |/ /") + "[-]", - o + tview.Escape(" | | | | | || (___ | ' / ") + "[-]", - o + tview.Escape(" | | | | | | \\___ \\ | < ") + "[-]", - o + tview.Escape(" | | | |__| | ____) || . \\ ") + "[-]", - o + tview.Escape(" |_| \\____/ |_____/ |_|\\_\\") + "[-]", + "", + "", + "", + c + ` _______ _ _ _______ _ _` + "[-]", + c + ` | | | |______ |____/ ` + "[-]", + c + ` | |_____| ______| | \_` + "[-]", + "", + "", + "", } uptimeStr := "--" @@ -436,7 +437,7 @@ func (a *App) updateHeader() { if headerWidth <= 0 { headerWidth = 120 } - logoWidth := 33 // visual width of the big letters + logoWidth := 34 // visual width of cyberlarge logo var lines []string for i := 0; i < len(infoLines) || i < len(logo); i++ { From b12e83e9c872ce01b94a010c692fa79844490a3c Mon Sep 17 00:00:00 2001 From: Fraser Isbester Date: Thu, 30 Apr 2026 09:44:21 -0700 Subject: [PATCH 2/3] feat: add tuskd daemon for continuous rule evaluation Adds a separate lightweight binary (tuskd) that runs the rule engine as a daemon, polling PostgreSQL and evaluating rules without any TUI dependencies. Uses the same config profiles and rule definitions. Co-Authored-By: Claude Opus 4.6 (1M context) --- Taskfile.yml | 19 ++++++++- cmd/tuskd/main.go | 103 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 cmd/tuskd/main.go diff --git a/Taskfile.yml b/Taskfile.yml index e153282..be2711b 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -16,12 +16,29 @@ tasks: generates: - ./tusk + build:daemon: + desc: Build the tuskd daemon binary + cmds: + - go build -o tuskd ./cmd/tuskd + sources: + - ./**/*.go + - go.mod + - go.sum + generates: + - ./tuskd + run: desc: Build and run tusk against the local dev database deps: [build] cmds: - ./tusk -P dev + daemon: + desc: Build and run the tuskd daemon against the local dev database + deps: [build:daemon] + cmds: + - ./tuskd -P dev + dev: desc: Build + run (alias) cmds: @@ -75,7 +92,7 @@ tasks: clean: desc: Clean build artifacts cmds: - - rm -f tusk + - rm -f tusk tuskd test: desc: Run all tests with Ginkgo diff --git a/cmd/tuskd/main.go b/cmd/tuskd/main.go new file mode 100644 index 0000000..653d278 --- /dev/null +++ b/cmd/tuskd/main.go @@ -0,0 +1,103 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + "syscall" + "time" + + "github.com/spf13/cobra" + + "github.com/fraser-isbester/tusk/internal/config" + "github.com/fraser-isbester/tusk/internal/db" + "github.com/fraser-isbester/tusk/internal/rules" +) + +func main() { + var profileFlag string + var intervalFlag time.Duration + + rootCmd := &cobra.Command{ + Use: "tuskd", + Short: "Tusk daemon — continuously monitors PostgreSQL and enforces rules", + Long: "A lightweight daemon that polls PostgreSQL and evaluates rules without any TUI dependencies.", + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := config.Load() + if err != nil { + return fmt.Errorf("loading config: %w", err) + } + + profile, err := cfg.ResolveProfile(profileFlag) + if err != nil { + return fmt.Errorf("resolving profile: %w", err) + } + + profileName := profileFlag + if profileName == "" { + profileName = cfg.DefaultProfile + } + + if len(profile.Rules) == 0 { + return fmt.Errorf("no rules configured in profile %q", profileName) + } + + connStr := profile.ConnectionString() + database, err := db.New(connStr) + if err != nil { + return fmt.Errorf("connecting to database: %w", err) + } + defer database.Close() + + compiled, err := rules.BuildRules(profile.Rules, profile.Readonly) + if err != nil { + return fmt.Errorf("compiling rules: %w", err) + } + + engine := rules.NewEngine(compiled, database, 5*time.Minute, 1000) + + log.Printf("tuskd: profile=%s rules=%d interval=%s", profileName, engine.RuleCount(), intervalFlag) + return runDaemon(engine, database, intervalFlag) + }, + } + + rootCmd.Flags().StringVarP(&profileFlag, "profile", "P", "", "connection profile name from config file") + rootCmd.Flags().DurationVarP(&intervalFlag, "interval", "i", 2*time.Second, "polling interval") + + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} + +func runDaemon(engine *rules.Engine, database *db.DB, interval time.Duration) error { + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer cancel() + + ticker := time.NewTicker(interval) + defer ticker.Stop() + + evaluate(ctx, engine, database) + + for { + select { + case <-ctx.Done(): + log.Println("tuskd: shutting down") + return nil + case <-ticker.C: + evaluate(ctx, engine, database) + } + } +} + +func evaluate(ctx context.Context, engine *rules.Engine, database *db.DB) { + queries, _ := database.GetActiveQueries(ctx) + txns, _ := database.GetTransactions(ctx) + locks, _ := database.GetLocks(ctx) + engine.Evaluate(ctx, rules.Snapshot{ + Queries: queries, + Transactions: txns, + Locks: locks, + }) +} From e9de27cb3469ebe20d284ea89179a575e7a4f969 Mon Sep 17 00:00:00 2001 From: Fraser Isbester Date: Thu, 30 Apr 2026 10:23:05 -0700 Subject: [PATCH 3/3] build: add tuskd to goreleaser config Co-Authored-By: Claude Opus 4.6 (1M context) --- .goreleaser.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 03d8dbb..6e5bc85 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,7 +1,8 @@ version: 2 builds: - - main: ./cmd/tusk + - id: tusk + main: ./cmd/tusk binary: tusk env: - CGO_ENABLED=0 @@ -14,6 +15,20 @@ builds: ldflags: - -s -w + - id: tuskd + main: ./cmd/tuskd + binary: tuskd + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + ldflags: + - -s -w + archives: - formats: - tar.gz