feat(polecat): Add gt polecat status command (gt-6lt3)
Implements detailed polecat status display including: - Lifecycle state (working, done, stuck, idle) - Assigned issue - Session status (running/stopped, attached/detached) - Session creation time - Last activity time with relative "ago" format Also extends tmux.SessionInfo and session.Info to include last activity timestamp from tmux session_activity. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/steveyegge/gastown/internal/config"
|
"github.com/steveyegge/gastown/internal/config"
|
||||||
@@ -169,9 +170,29 @@ Examples:
|
|||||||
RunE: runPolecatSync,
|
RunE: runPolecatSync,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var polecatStatusCmd = &cobra.Command{
|
||||||
|
Use: "status <rig>/<polecat>",
|
||||||
|
Short: "Show detailed status for a polecat",
|
||||||
|
Long: `Show detailed status for a polecat.
|
||||||
|
|
||||||
|
Displays comprehensive information including:
|
||||||
|
- Current lifecycle state (working, done, stuck, idle)
|
||||||
|
- Assigned issue (if any)
|
||||||
|
- Session status (running/stopped, attached/detached)
|
||||||
|
- Session creation time
|
||||||
|
- Last activity time
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
gt polecat status gastown/Toast
|
||||||
|
gt polecat status gastown/Toast --json`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: runPolecatStatus,
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
polecatSyncAll bool
|
polecatSyncAll bool
|
||||||
polecatSyncFromMain bool
|
polecatSyncFromMain bool
|
||||||
|
polecatStatusJSON bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -186,6 +207,9 @@ func init() {
|
|||||||
polecatSyncCmd.Flags().BoolVar(&polecatSyncAll, "all", false, "Sync all polecats in the rig")
|
polecatSyncCmd.Flags().BoolVar(&polecatSyncAll, "all", false, "Sync all polecats in the rig")
|
||||||
polecatSyncCmd.Flags().BoolVar(&polecatSyncFromMain, "from-main", false, "Pull only, no push")
|
polecatSyncCmd.Flags().BoolVar(&polecatSyncFromMain, "from-main", false, "Pull only, no push")
|
||||||
|
|
||||||
|
// Status flags
|
||||||
|
polecatStatusCmd.Flags().BoolVar(&polecatStatusJSON, "json", false, "Output as JSON")
|
||||||
|
|
||||||
// Add subcommands
|
// Add subcommands
|
||||||
polecatCmd.AddCommand(polecatListCmd)
|
polecatCmd.AddCommand(polecatListCmd)
|
||||||
polecatCmd.AddCommand(polecatAddCmd)
|
polecatCmd.AddCommand(polecatAddCmd)
|
||||||
@@ -195,6 +219,7 @@ func init() {
|
|||||||
polecatCmd.AddCommand(polecatDoneCmd)
|
polecatCmd.AddCommand(polecatDoneCmd)
|
||||||
polecatCmd.AddCommand(polecatResetCmd)
|
polecatCmd.AddCommand(polecatResetCmd)
|
||||||
polecatCmd.AddCommand(polecatSyncCmd)
|
polecatCmd.AddCommand(polecatSyncCmd)
|
||||||
|
polecatCmd.AddCommand(polecatStatusCmd)
|
||||||
|
|
||||||
rootCmd.AddCommand(polecatCmd)
|
rootCmd.AddCommand(polecatCmd)
|
||||||
}
|
}
|
||||||
@@ -577,3 +602,152 @@ func runPolecatSync(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PolecatStatus represents detailed polecat status for JSON output.
|
||||||
|
type PolecatStatus struct {
|
||||||
|
Rig string `json:"rig"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
State polecat.State `json:"state"`
|
||||||
|
Issue string `json:"issue,omitempty"`
|
||||||
|
ClonePath string `json:"clone_path"`
|
||||||
|
Branch string `json:"branch"`
|
||||||
|
SessionRunning bool `json:"session_running"`
|
||||||
|
SessionID string `json:"session_id,omitempty"`
|
||||||
|
Attached bool `json:"attached,omitempty"`
|
||||||
|
Windows int `json:"windows,omitempty"`
|
||||||
|
CreatedAt string `json:"created_at,omitempty"`
|
||||||
|
LastActivity string `json:"last_activity,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func runPolecatStatus(cmd *cobra.Command, args []string) error {
|
||||||
|
rigName, polecatName, err := parseAddress(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr, r, err := getPolecatManager(rigName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get polecat info
|
||||||
|
p, err := mgr.Get(polecatName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("polecat '%s' not found in rig '%s'", polecatName, rigName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get session info
|
||||||
|
t := tmux.NewTmux()
|
||||||
|
sessMgr := session.NewManager(t, r)
|
||||||
|
sessInfo, err := sessMgr.Status(polecatName)
|
||||||
|
if err != nil {
|
||||||
|
// Non-fatal - continue without session info
|
||||||
|
sessInfo = &session.Info{
|
||||||
|
Polecat: polecatName,
|
||||||
|
Running: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON output
|
||||||
|
if polecatStatusJSON {
|
||||||
|
status := PolecatStatus{
|
||||||
|
Rig: rigName,
|
||||||
|
Name: polecatName,
|
||||||
|
State: p.State,
|
||||||
|
Issue: p.Issue,
|
||||||
|
ClonePath: p.ClonePath,
|
||||||
|
Branch: p.Branch,
|
||||||
|
SessionRunning: sessInfo.Running,
|
||||||
|
SessionID: sessInfo.SessionID,
|
||||||
|
Attached: sessInfo.Attached,
|
||||||
|
Windows: sessInfo.Windows,
|
||||||
|
}
|
||||||
|
if !sessInfo.Created.IsZero() {
|
||||||
|
status.CreatedAt = sessInfo.Created.Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
if !sessInfo.LastActivity.IsZero() {
|
||||||
|
status.LastActivity = sessInfo.LastActivity.Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
return enc.Encode(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Human-readable output
|
||||||
|
fmt.Printf("%s\n\n", style.Bold.Render(fmt.Sprintf("Polecat: %s/%s", rigName, polecatName)))
|
||||||
|
|
||||||
|
// State with color
|
||||||
|
stateStr := string(p.State)
|
||||||
|
switch p.State {
|
||||||
|
case polecat.StateWorking:
|
||||||
|
stateStr = style.Info.Render(stateStr)
|
||||||
|
case polecat.StateStuck:
|
||||||
|
stateStr = style.Warning.Render(stateStr)
|
||||||
|
case polecat.StateDone:
|
||||||
|
stateStr = style.Success.Render(stateStr)
|
||||||
|
default:
|
||||||
|
stateStr = style.Dim.Render(stateStr)
|
||||||
|
}
|
||||||
|
fmt.Printf(" State: %s\n", stateStr)
|
||||||
|
|
||||||
|
// Issue
|
||||||
|
if p.Issue != "" {
|
||||||
|
fmt.Printf(" Issue: %s\n", p.Issue)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" Issue: %s\n", style.Dim.Render("(none)"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone path and branch
|
||||||
|
fmt.Printf(" Clone: %s\n", style.Dim.Render(p.ClonePath))
|
||||||
|
fmt.Printf(" Branch: %s\n", style.Dim.Render(p.Branch))
|
||||||
|
|
||||||
|
// Session info
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("%s\n", style.Bold.Render("Session"))
|
||||||
|
|
||||||
|
if sessInfo.Running {
|
||||||
|
fmt.Printf(" Status: %s\n", style.Success.Render("running"))
|
||||||
|
fmt.Printf(" Session ID: %s\n", style.Dim.Render(sessInfo.SessionID))
|
||||||
|
|
||||||
|
if sessInfo.Attached {
|
||||||
|
fmt.Printf(" Attached: %s\n", style.Info.Render("yes"))
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" Attached: %s\n", style.Dim.Render("no"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if sessInfo.Windows > 0 {
|
||||||
|
fmt.Printf(" Windows: %d\n", sessInfo.Windows)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !sessInfo.Created.IsZero() {
|
||||||
|
fmt.Printf(" Created: %s\n", sessInfo.Created.Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !sessInfo.LastActivity.IsZero() {
|
||||||
|
// Show relative time for activity
|
||||||
|
ago := formatActivityTime(sessInfo.LastActivity)
|
||||||
|
fmt.Printf(" Last Activity: %s (%s)\n",
|
||||||
|
sessInfo.LastActivity.Format("15:04:05"),
|
||||||
|
style.Dim.Render(ago))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" Status: %s\n", style.Dim.Render("not running"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatActivityTime returns a human-readable relative time string.
|
||||||
|
func formatActivityTime(t time.Time) string {
|
||||||
|
d := time.Since(t)
|
||||||
|
switch {
|
||||||
|
case d < time.Minute:
|
||||||
|
return fmt.Sprintf("%d seconds ago", int(d.Seconds()))
|
||||||
|
case d < time.Hour:
|
||||||
|
return fmt.Sprintf("%d minutes ago", int(d.Minutes()))
|
||||||
|
case d < 24*time.Hour:
|
||||||
|
return fmt.Sprintf("%d hours ago", int(d.Hours()))
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%d days ago", int(d.Hours()/24))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -69,6 +69,9 @@ type Info struct {
|
|||||||
|
|
||||||
// Windows is the number of tmux windows.
|
// Windows is the number of tmux windows.
|
||||||
Windows int `json:"windows,omitempty"`
|
Windows int `json:"windows,omitempty"`
|
||||||
|
|
||||||
|
// LastActivity is when the session last had activity.
|
||||||
|
LastActivity time.Time `json:"last_activity,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// sessionName generates the tmux session name for a polecat.
|
// sessionName generates the tmux session name for a polecat.
|
||||||
@@ -254,6 +257,14 @@ func (m *Manager) Status(polecat string) (*Info, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse activity time (unix timestamp from tmux)
|
||||||
|
if tmuxInfo.Activity != "" {
|
||||||
|
var activityUnix int64
|
||||||
|
if _, err := fmt.Sscanf(tmuxInfo.Activity, "%d", &activityUnix); err == nil && activityUnix > 0 {
|
||||||
|
info.LastActivity = time.Unix(activityUnix, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return info, nil
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -218,10 +218,12 @@ func (t *Tmux) RenameSession(oldName, newName string) error {
|
|||||||
|
|
||||||
// SessionInfo contains information about a tmux session.
|
// SessionInfo contains information about a tmux session.
|
||||||
type SessionInfo struct {
|
type SessionInfo struct {
|
||||||
Name string
|
Name string
|
||||||
Windows int
|
Windows int
|
||||||
Created string
|
Created string
|
||||||
Attached bool
|
Attached bool
|
||||||
|
Activity string // Last activity time
|
||||||
|
LastAttached string // Last time the session was attached
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisplayMessage shows a message in the tmux status line.
|
// DisplayMessage shows a message in the tmux status line.
|
||||||
@@ -316,7 +318,7 @@ func (t *Tmux) WaitForShellReady(session string, timeout time.Duration) error {
|
|||||||
|
|
||||||
// GetSessionInfo returns detailed information about a session.
|
// GetSessionInfo returns detailed information about a session.
|
||||||
func (t *Tmux) GetSessionInfo(name string) (*SessionInfo, error) {
|
func (t *Tmux) GetSessionInfo(name string) (*SessionInfo, error) {
|
||||||
format := "#{session_name}|#{session_windows}|#{session_created_string}|#{session_attached}"
|
format := "#{session_name}|#{session_windows}|#{session_created_string}|#{session_attached}|#{session_activity}|#{session_last_attached}"
|
||||||
out, err := t.run("list-sessions", "-F", format, "-f", fmt.Sprintf("#{==:#{session_name},%s}", name))
|
out, err := t.run("list-sessions", "-F", format, "-f", fmt.Sprintf("#{==:#{session_name},%s}", name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -326,19 +328,29 @@ func (t *Tmux) GetSessionInfo(name string) (*SessionInfo, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.Split(out, "|")
|
parts := strings.Split(out, "|")
|
||||||
if len(parts) != 4 {
|
if len(parts) < 4 {
|
||||||
return nil, fmt.Errorf("unexpected session info format: %s", out)
|
return nil, fmt.Errorf("unexpected session info format: %s", out)
|
||||||
}
|
}
|
||||||
|
|
||||||
windows := 0
|
windows := 0
|
||||||
_, _ = fmt.Sscanf(parts[1], "%d", &windows)
|
_, _ = fmt.Sscanf(parts[1], "%d", &windows)
|
||||||
|
|
||||||
return &SessionInfo{
|
info := &SessionInfo{
|
||||||
Name: parts[0],
|
Name: parts[0],
|
||||||
Windows: windows,
|
Windows: windows,
|
||||||
Created: parts[2],
|
Created: parts[2],
|
||||||
Attached: parts[3] == "1",
|
Attached: parts[3] == "1",
|
||||||
}, nil
|
}
|
||||||
|
|
||||||
|
// Activity and last attached are optional (may not be present in older tmux)
|
||||||
|
if len(parts) > 4 {
|
||||||
|
info.Activity = parts[4]
|
||||||
|
}
|
||||||
|
if len(parts) > 5 {
|
||||||
|
info.LastAttached = parts[5]
|
||||||
|
}
|
||||||
|
|
||||||
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplyTheme sets the status bar style for a session.
|
// ApplyTheme sets the status bar style for a session.
|
||||||
|
|||||||
Reference in New Issue
Block a user