package cmd import ( "fmt" "os" "path/filepath" "time" "github.com/spf13/cobra" "github.com/warrenronsiek/ctask/internal/config" "github.com/warrenronsiek/ctask/internal/session" "github.com/warrenronsiek/ctask/internal/workspace" ) var archiveCmd = &cobra.Command{ Use: "archive ", Short: "Mark a workspace as archived", Args: cobra.ExactArgs(1), SilenceUsage: true, RunE: runArchive, } func init() { archiveCmd.ValidArgsFunction = completeWorkspaces(completionActive) rootCmd.AddCommand(archiveCmd) } func runArchive(cmd *cobra.Command, args []string) error { roots := config.SearchRoots() ws := resolveOne(roots, args[0], false) // Active-session check: a fresh lease means a session is actively writing // to this workspace. On TTY stdin we prompt. On non-TTY stdin we refuse — // archiving under an active session silently is the most surprising // failure mode, so the safe default is to exit non-zero. if lease, err := session.ReadLease(session.LeasePath(ws.Path)); err == nil && lease != nil { if session.IsFresh(lease, time.Now(), session.StaleLeaseAfter) { fmt.Print(formatArchiveActiveWarning(lease, time.Now())) if !isStdinTerminal() { fmt.Println() return fmt.Errorf("refusing to archive workspace with active session (stdin is not a terminal)") } if !session.ConfirmYN(os.Stdin, os.Stdout, " Archive anyway? [y/N] ", false) { fmt.Println(" Cancelled.") return nil } } } now := time.Now().UTC().Truncate(time.Second) ws.Meta.Status = "archived" ws.Meta.ArchivedAt = &now ws.Meta.UpdatedAt = now metaPath := filepath.Join(ws.Path, "task.yaml") if err := workspace.WriteMetaLocked(metaPath, ws.Meta); err != nil { return fmt.Errorf("updating metadata: %w", err) } relPath := workspace.RelativePath(ws.Root, ws.Path) fmt.Printf("[ctask] archived: %s\n", relPath) return nil } // formatArchiveActiveWarning renders the warning block shown when archive // detects a fresh lease. func formatArchiveActiveWarning(l *session.Lease, now time.Time) string { startedAgo := now.Sub(l.StartedAt) return fmt.Sprintf( "[ctask] Warning: this workspace has an active session:\n"+ " Host: %s\n"+ " Agent: %s\n"+ " Started: %s (%s ago)\n\n"+ " Archiving will hide it from list and resume.\n"+ " The active session will continue but may behave unexpectedly.\n\n", l.Hostname, l.Agent, l.StartedAt.Local().Format("2006-01-02 15:04"), session.FormatAgo(startedAgo), ) } // isStdinTerminal reports whether os.Stdin is attached to a terminal. // Uses os.Stdin.Stat + ModeCharDevice to avoid adding a golang.org/x/term // dependency. Pipes and redirected files lack the ModeCharDevice bit on // every supported platform. func isStdinTerminal() bool { fi, err := os.Stdin.Stat() if err != nil { return false } return (fi.Mode() & os.ModeCharDevice) != 0 }