feat: all six CLI commands (new, list, resume, open, info, archive)
Complete command implementations with all flags per spec. Shared query resolution helper. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/warrenronsiek/ctask/internal/config"
|
||||
"github.com/warrenronsiek/ctask/internal/workspace"
|
||||
)
|
||||
|
||||
var archiveCmd = &cobra.Command{
|
||||
Use: "archive <query>",
|
||||
Short: "Mark a workspace as archived",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runArchive,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(archiveCmd)
|
||||
}
|
||||
|
||||
func runArchive(cmd *cobra.Command, args []string) error {
|
||||
root := config.ResolveRoot()
|
||||
ws := resolveOne(root, args[0], false)
|
||||
|
||||
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.WriteMeta(metaPath, ws.Meta); err != nil {
|
||||
return fmt.Errorf("updating metadata: %w", err)
|
||||
}
|
||||
|
||||
relPath := workspace.RelativePath(root, ws.Path)
|
||||
fmt.Printf("[ctask] archived: %s\n", relPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/warrenronsiek/ctask/internal/workspace"
|
||||
)
|
||||
|
||||
// resolveOne resolves a query to exactly one workspace. Prints errors and exits on 0 or >1 matches.
|
||||
func resolveOne(root, query string, includeArchived bool) *workspace.QueryResult {
|
||||
results, err := workspace.ResolveQuery(root, query, includeArchived)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "No workspace matches %q.\n", query)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(results) > 1 {
|
||||
fmt.Fprintf(os.Stderr, "Multiple workspaces match %q:\n", query)
|
||||
for _, r := range results {
|
||||
fmt.Fprintf(os.Stderr, " %s\n", workspace.RelativePath(root, r.Path))
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, "Specify a more precise query.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return &results[0]
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/warrenronsiek/ctask/internal/config"
|
||||
)
|
||||
|
||||
var infoCmd = &cobra.Command{
|
||||
Use: "info <query>",
|
||||
Short: "Display metadata and path for a workspace",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runInfo,
|
||||
}
|
||||
|
||||
var infoAll bool
|
||||
|
||||
func init() {
|
||||
infoCmd.Flags().BoolVarP(&infoAll, "all", "a", false, "Include archived workspaces in query resolution")
|
||||
rootCmd.AddCommand(infoCmd)
|
||||
}
|
||||
|
||||
func runInfo(cmd *cobra.Command, args []string) error {
|
||||
root := config.ResolveRoot()
|
||||
ws := resolveOne(root, args[0], infoAll)
|
||||
m := ws.Meta
|
||||
|
||||
fmt.Printf("Task: %s\n", m.Slug)
|
||||
fmt.Printf("Title: %s\n", m.Title)
|
||||
fmt.Printf("Category: %s\n", m.Category)
|
||||
fmt.Printf("Status: %s\n", m.Status)
|
||||
fmt.Printf("Mode: %s\n", m.Mode)
|
||||
fmt.Printf("Agent: %s\n", m.Agent)
|
||||
fmt.Printf("Created: %s\n", m.CreatedAt.Format("2006-01-02 15:04:05"))
|
||||
fmt.Printf("Updated: %s\n", m.UpdatedAt.Format("2006-01-02 15:04:05"))
|
||||
fmt.Printf("Path: %s\n", ws.Path)
|
||||
|
||||
if m.ArchivedAt != nil {
|
||||
fmt.Printf("Archived: %s\n", m.ArchivedAt.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
// List contents
|
||||
fmt.Println()
|
||||
fmt.Println("Contents:")
|
||||
entries, err := os.ReadDir(ws.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, e := range entries {
|
||||
name := e.Name()
|
||||
if e.IsDir() {
|
||||
name += "/"
|
||||
}
|
||||
fmt.Printf(" %s\n", name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/warrenronsiek/ctask/internal/config"
|
||||
"github.com/warrenronsiek/ctask/internal/workspace"
|
||||
)
|
||||
|
||||
var listCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "Show recent workspaces in reverse-chronological order",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: runList,
|
||||
}
|
||||
|
||||
var (
|
||||
listAll bool
|
||||
listCategory string
|
||||
listLimit int
|
||||
)
|
||||
|
||||
func init() {
|
||||
listCmd.Flags().BoolVarP(&listAll, "all", "a", false, "Include archived workspaces")
|
||||
listCmd.Flags().StringVarP(&listCategory, "category", "c", "", "Filter by category")
|
||||
listCmd.Flags().IntVarP(&listLimit, "limit", "n", 20, "Maximum entries to show")
|
||||
rootCmd.AddCommand(listCmd)
|
||||
}
|
||||
|
||||
func runList(cmd *cobra.Command, args []string) error {
|
||||
root := config.ResolveRoot()
|
||||
|
||||
results, err := workspace.ListWorkspaces(root, workspace.ListOpts{
|
||||
IncludeArchived: listAll,
|
||||
Category: listCategory,
|
||||
Limit: listLimit,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
fmt.Println("No workspaces found.")
|
||||
return nil
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
for _, ws := range results {
|
||||
dirName := filepath.Base(ws.Path)
|
||||
date := ""
|
||||
if len(dirName) >= 10 {
|
||||
date = dirName[:10]
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
|
||||
ws.Meta.Status,
|
||||
ws.Meta.Mode,
|
||||
ws.Meta.Category,
|
||||
date,
|
||||
ws.Meta.Slug,
|
||||
)
|
||||
}
|
||||
w.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/warrenronsiek/ctask/internal/config"
|
||||
"github.com/warrenronsiek/ctask/internal/shell"
|
||||
"github.com/warrenronsiek/ctask/internal/workspace"
|
||||
)
|
||||
|
||||
var newCmd = &cobra.Command{
|
||||
Use: "new [title]",
|
||||
Short: "Create a new task workspace and launch the agent",
|
||||
Long: "Create a new task workspace and launch the agent. If title is omitted, generates task-HHMMSS.",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: runNew,
|
||||
}
|
||||
|
||||
var (
|
||||
newCategory string
|
||||
newContainer bool
|
||||
newShell bool
|
||||
newAgent string
|
||||
newNoLaunch bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
newCmd.Flags().StringVarP(&newCategory, "category", "c", "general", "Workspace category subdirectory")
|
||||
newCmd.Flags().BoolVar(&newContainer, "container", false, "Launch in container sandbox (v0.2)")
|
||||
newCmd.Flags().BoolVar(&newShell, "shell", false, "Open interactive shell instead of agent")
|
||||
newCmd.Flags().StringVarP(&newAgent, "agent", "a", "", "Command to exec as the agent")
|
||||
newCmd.Flags().BoolVar(&newNoLaunch, "no-launch", false, "Create workspace only, do not launch")
|
||||
rootCmd.AddCommand(newCmd)
|
||||
}
|
||||
|
||||
func runNew(cmd *cobra.Command, args []string) error {
|
||||
if newContainer {
|
||||
fmt.Println(shell.ContainerNotice())
|
||||
return nil
|
||||
}
|
||||
|
||||
root := config.ResolveRoot()
|
||||
agent := newAgent
|
||||
if agent == "" {
|
||||
agent = config.ResolveAgent()
|
||||
}
|
||||
|
||||
title := ""
|
||||
if len(args) > 0 {
|
||||
title = args[0]
|
||||
}
|
||||
|
||||
ws, err := workspace.Create(workspace.CreateOpts{
|
||||
Root: root,
|
||||
Title: title,
|
||||
Category: newCategory,
|
||||
Mode: "local",
|
||||
Agent: agent,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relPath := workspace.RelativePath(root, ws.Path)
|
||||
fmt.Printf("[ctask] created %s\n", relPath)
|
||||
|
||||
if newNoLaunch {
|
||||
return nil
|
||||
}
|
||||
|
||||
envVars := config.EnvVars(ws.Meta.Slug, ws.Meta.Mode, root, ws.Path, ws.Meta.Category)
|
||||
|
||||
if newShell {
|
||||
return shell.ExecShell(ws.Path, envVars, ws.Meta.Slug, ws.Meta.Mode)
|
||||
}
|
||||
|
||||
// Agent mode: print banner and exec
|
||||
for _, line := range shell.BannerLines(ws.Meta.Mode, ws.Meta.Slug, ws.Path) {
|
||||
fmt.Println(line)
|
||||
}
|
||||
|
||||
return shell.ExecAgent(agent, ws.Path, envVars)
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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 openCmd = &cobra.Command{
|
||||
Use: "open <query>",
|
||||
Short: "Open a workspace directory without launching the agent",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runOpen,
|
||||
}
|
||||
|
||||
var openAll bool
|
||||
|
||||
func init() {
|
||||
openCmd.Flags().BoolVarP(&openAll, "all", "a", false, "Include archived workspaces in query resolution")
|
||||
rootCmd.AddCommand(openCmd)
|
||||
}
|
||||
|
||||
func runOpen(cmd *cobra.Command, args []string) error {
|
||||
root := config.ResolveRoot()
|
||||
ws := resolveOne(root, args[0], openAll)
|
||||
|
||||
// Update updated_at
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
ws.Meta.UpdatedAt = now
|
||||
metaPath := filepath.Join(ws.Path, "task.yaml")
|
||||
if err := workspace.WriteMeta(metaPath, ws.Meta); err != nil {
|
||||
return fmt.Errorf("updating metadata: %w", err)
|
||||
}
|
||||
|
||||
envVars := config.EnvVars(ws.Meta.Slug, ws.Meta.Mode, root, ws.Path, ws.Meta.Category)
|
||||
|
||||
// Spawn interactive subshell (not agent, not cd in parent)
|
||||
return shell.ExecShell(ws.Path, envVars, ws.Meta.Slug, ws.Meta.Mode)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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),
|
||||
RunE: runResume,
|
||||
}
|
||||
|
||||
var (
|
||||
resumeContainer bool
|
||||
resumeShell bool
|
||||
resumeAgent string
|
||||
)
|
||||
|
||||
func init() {
|
||||
resumeCmd.Flags().BoolVar(&resumeContainer, "container", false, "Resume in container mode (v0.2)")
|
||||
resumeCmd.Flags().BoolVar(&resumeShell, "shell", false, "Open shell instead of agent")
|
||||
resumeCmd.Flags().StringVarP(&resumeAgent, "agent", "a", "", "Override agent command")
|
||||
rootCmd.AddCommand(resumeCmd)
|
||||
}
|
||||
|
||||
func runResume(cmd *cobra.Command, args []string) error {
|
||||
if resumeContainer {
|
||||
fmt.Println(shell.ContainerNotice())
|
||||
return nil
|
||||
}
|
||||
|
||||
root := config.ResolveRoot()
|
||||
ws := resolveOne(root, args[0], false)
|
||||
|
||||
// Update updated_at
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
ws.Meta.UpdatedAt = now
|
||||
metaPath := filepath.Join(ws.Path, "task.yaml")
|
||||
if err := workspace.WriteMeta(metaPath, ws.Meta); err != nil {
|
||||
return fmt.Errorf("updating metadata: %w", err)
|
||||
}
|
||||
|
||||
agent := resumeAgent
|
||||
if agent == "" {
|
||||
agent = ws.Meta.Agent
|
||||
}
|
||||
|
||||
envVars := config.EnvVars(ws.Meta.Slug, ws.Meta.Mode, root, ws.Path, ws.Meta.Category)
|
||||
|
||||
if resumeShell {
|
||||
return shell.ExecShell(ws.Path, envVars, ws.Meta.Slug, ws.Meta.Mode)
|
||||
}
|
||||
|
||||
// Agent mode
|
||||
for _, line := range shell.BannerLines(ws.Meta.Mode, ws.Meta.Slug, ws.Path) {
|
||||
fmt.Println(line)
|
||||
}
|
||||
|
||||
return shell.ExecAgent(agent, ws.Path, envVars)
|
||||
}
|
||||
Reference in New Issue
Block a user