ae9bfafb1f
ctask resume <archived-workspace> printed both the helpful [ctask] diagnostic + restore hint AND a redundant trailing "Error: workspace archived" line from Cobra's default error rendering. Cosmetic but unprofessional. Add an errArchivedWorkspace sentinel and have runResume flip SilenceErrors only when the inner error is that sentinel. All other resume errors (lookup failure, metadata write failure, etc.) continue to flow through Cobra's default rendering unchanged — we only silence the case where we have already printed an equivalent diagnostic ourselves. Verified end-to-end through Cobra Execute (not just runResume direct invocation) so the SilenceErrors-flip-from-RunE timing is exercised.
119 lines
4.0 KiB
Go
119 lines
4.0 KiB
Go
package cmd
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/warrenronsiek/ctask/internal/config"
|
|
"github.com/warrenronsiek/ctask/internal/shell"
|
|
"github.com/warrenronsiek/ctask/internal/workspace"
|
|
)
|
|
|
|
var resumeCmd = &cobra.Command{
|
|
Use: "resume <query>",
|
|
Short: "Reopen an existing workspace and launch the agent",
|
|
Args: cobra.ExactArgs(1),
|
|
SilenceUsage: true,
|
|
RunE: runResume,
|
|
}
|
|
|
|
var (
|
|
resumeContainer bool
|
|
resumeShell bool
|
|
resumeAgent string
|
|
resumeForce bool
|
|
resumeDirect bool
|
|
)
|
|
|
|
func init() {
|
|
resumeCmd.Flags().BoolVar(&resumeContainer, "container", false, "Resume in container mode (deferred)")
|
|
resumeCmd.Flags().BoolVar(&resumeShell, "shell", false, "Open shell instead of agent")
|
|
resumeCmd.Flags().StringVarP(&resumeAgent, "agent", "a", "", "Override agent command")
|
|
resumeCmd.Flags().BoolVar(&resumeForce, "force", false, "Skip active-session and stale-workspace warnings")
|
|
resumeCmd.Flags().BoolVar(&resumeDirect, "direct", false, "Bypass persistent session mode for this command")
|
|
resumeCmd.ValidArgsFunction = completeWorkspaces(completionActive)
|
|
rootCmd.AddCommand(resumeCmd)
|
|
}
|
|
|
|
// errArchivedWorkspace is the sentinel doResume returns when the user
|
|
// asks to resume an archived workspace. doResume already prints the
|
|
// full [ctask] diagnostic + restore hint to stderr, so the cmd-layer
|
|
// wrapper flips SilenceErrors to suppress Cobra's redundant trailing
|
|
// "Error: workspace archived" line. All other errors (lookup failure,
|
|
// metadata write failure, etc.) flow through Cobra's default
|
|
// rendering unchanged.
|
|
var errArchivedWorkspace = errors.New("workspace archived")
|
|
|
|
func runResume(cmd *cobra.Command, args []string) error {
|
|
err := doResume(args[0], resumeContainer, resumeShell, resumeForce, resumeAgent, resumeDirect)
|
|
if errors.Is(err, errArchivedWorkspace) {
|
|
cmd.SilenceErrors = true
|
|
}
|
|
return err
|
|
}
|
|
|
|
// doResume is the shared resume logic used by both `resume` and `last`.
|
|
// It preserves resume's existing archive-inclusive resolution and
|
|
// restore-hint behavior, then delegates to runWorkspaceEntry for the
|
|
// persistent-vs-direct decision and tmux dispatch.
|
|
func doResume(query string, container, useShell, force bool, agentOverride string, directFlag bool) error {
|
|
if container {
|
|
fmt.Println(shell.ContainerNotice())
|
|
return nil
|
|
}
|
|
|
|
roots := config.SearchRoots()
|
|
// resume resolves archived-inclusive so we can give a helpful hint when
|
|
// the user resumes an archived workspace (v0.5.2 behavior — preserved).
|
|
ws := resolveOne(roots, query, true)
|
|
|
|
if ws.Meta.Status == "archived" {
|
|
fmt.Fprint(os.Stderr, formatResumeRestoreHint(query))
|
|
return errArchivedWorkspace
|
|
}
|
|
|
|
// updated_at bump (existing v0.4 behavior).
|
|
now := time.Now().UTC().Truncate(time.Second)
|
|
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)
|
|
}
|
|
|
|
agent := agentOverride
|
|
if agent == "" {
|
|
agent = ws.Meta.Agent
|
|
}
|
|
|
|
return runWorkspaceEntry(WorkspaceEntryOptions{
|
|
WsPath: ws.Path,
|
|
WsRoot: ws.Root,
|
|
WsMeta: ws.Meta,
|
|
Agent: agent,
|
|
Shell: useShell,
|
|
Force: force,
|
|
Direct: directFlag,
|
|
CommandName: "resume",
|
|
})
|
|
}
|
|
|
|
// formatResumeRestoreHint builds the multi-line stderr block printed
|
|
// when `ctask resume <query>` resolves to an archived workspace.
|
|
// Extracted so the v0.5.4 invocation-name audit can verify the
|
|
// command-form line uses invocationName() without depending on the
|
|
// surrounding fmt.Fprintf machinery.
|
|
//
|
|
// The "[ctask]" diagnostic prefix is intentionally a literal product
|
|
// reference (spec §2: product-identity references stay literal). The
|
|
// `restore <query>` line is the command-form portion and uses
|
|
// invocationName().
|
|
func formatResumeRestoreHint(query string) string {
|
|
return fmt.Sprintf(
|
|
"[ctask] error: workspace %q is archived\n\nTo restore it:\n %s restore %s\n",
|
|
query, invocationName(), query)
|
|
}
|