// Command harness is the terminal front end for the Paperclip coding factory. // // This file only wires flags and starts the program. The screens live in // model.go. Every fact the program shows comes from the Paperclip API; nothing // is stored locally. package main import ( "flag" "fmt" "os" tea "github.com/charmbracelet/bubbletea" "paperclip-harness/tui/paperclip" ) func main() { project := flag.String("project", "", "project id or url key (for example spike-2026-11-10)") dump := flag.Bool("dump", false, "print the task tree as plain text and exit (no full-screen interface)") flag.Parse() client, err := paperclip.New() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } if !client.UsingUserKey() { fmt.Fprintln(os.Stderr, "warning: PAPERCLIP_BOARD_KEY is not set, so replies and status changes will be refused") } proj, err := client.ResolveProject(*project) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } if *dump { if err := dumpTree(client, proj); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } return } // Alternate screen: TUIOS keeps its own scrollback, and a full-screen // program that redraws in place must not fight it. p := tea.NewProgram(newModel(client, proj), tea.WithAltScreen(), tea.WithMouseCellMotion(), ) if _, err := p.Run(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } // dumpTree prints the tree without starting the interface. It is how the data // path is checked from a script or over ssh. func dumpTree(c *paperclip.Client, proj paperclip.Project) error { rows, err := loadRows(c, proj) if err != nil { return err } fmt.Printf("%s (%s)\n\n", proj.Name, proj.Status) for _, g := range groupOrder { any := false for _, r := range rows { if r.Group != g { continue } if !any { fmt.Printf("%s\n", g) any = true } fmt.Printf(" %-8s %-12s %-14s %s\n", r.Issue.Identifier, r.Issue.Status, r.Who, r.Issue.Title) if r.Blocker != nil && r.Blocker.UnresolvedCount > 0 { fmt.Printf(" blocked by %v\n", r.Blocker.BlockingIdentities) } } if any { fmt.Println() } } return nil }