package cmd import ( "context" "encoding/json" "fmt" "os" "gogs.dmsc.dev/arp/arp_cli/client" "gogs.dmsc.dev/arp/arp_cli/config" "github.com/AlecAivazis/survey/v2" "github.com/olekukonko/tablewriter" "github.com/urfave/cli/v3" ) // TaskCommand returns the task command func TaskCommand() *cli.Command { return &cli.Command{ Name: "task", Usage: "Manage tasks", Description: `Manage ARP tasks. Tasks are work items that can be assigned to users. Tasks have statuses, priorities, and due dates. Use this command to create, list, update, and delete tasks. You can also watch for real-time task updates.`, Commands: []*cli.Command{ { Name: "list", Aliases: []string{"ls"}, Usage: "List all tasks", Flags: []cli.Flag{ &cli.BoolFlag{ Name: "json", Aliases: []string{"j"}, Usage: "Output as JSON", }, }, Action: taskList, }, { Name: "get", Usage: "Get a task by ID", Flags: []cli.Flag{ &cli.StringFlag{ Name: "id", Aliases: []string{"i"}, Usage: "Task ID", Required: true, }, &cli.BoolFlag{ Name: "json", Aliases: []string{"j"}, Usage: "Output as JSON", }, }, Action: taskGet, }, { Name: "create", Usage: "Create a new task", Action: taskCreate, Flags: []cli.Flag{ &cli.StringFlag{ Name: "title", Aliases: []string{"t"}, Usage: "Task title", }, &cli.StringFlag{ Name: "content", Aliases: []string{"c"}, Usage: "Task content", }, &cli.StringFlag{ Name: "created-by", Aliases: []string{"b"}, Usage: "Creator user ID", }, &cli.StringFlag{ Name: "assignee", Aliases: []string{"a"}, Usage: "Assignee user ID", }, &cli.StringFlag{ Name: "status", Aliases: []string{"s"}, Usage: "Status ID", }, &cli.StringFlag{ Name: "due-date", Aliases: []string{"d"}, Usage: "Due date", }, &cli.StringFlag{ Name: "priority", Aliases: []string{"p"}, Usage: "Priority (low, medium, high)", }, }, }, { Name: "update", Usage: "Update a task", Action: taskUpdate, Flags: []cli.Flag{ &cli.StringFlag{ Name: "id", Aliases: []string{"i"}, Usage: "Task ID", Required: true, }, &cli.StringFlag{ Name: "title", Aliases: []string{"t"}, Usage: "Task title", }, &cli.StringFlag{ Name: "content", Aliases: []string{"c"}, Usage: "Task content", }, &cli.StringFlag{ Name: "assignee", Aliases: []string{"a"}, Usage: "Assignee user ID", }, &cli.StringFlag{ Name: "status", Aliases: []string{"s"}, Usage: "Status ID", }, &cli.StringFlag{ Name: "due-date", Aliases: []string{"d"}, Usage: "Due date", }, &cli.StringFlag{ Name: "priority", Aliases: []string{"p"}, Usage: "Priority", }, }, }, { Name: "delete", Usage: "Delete a task", Action: taskDelete, Flags: []cli.Flag{ &cli.StringFlag{ Name: "id", Aliases: []string{"i"}, Usage: "Task ID", Required: true, }, &cli.BoolFlag{ Name: "yes", Aliases: []string{"y"}, Usage: "Skip confirmation", }, }, }, { Name: "watch", Usage: "Watch for real-time task updates", Action: taskWatch, Flags: []cli.Flag{ &cli.StringFlag{ Name: "event", Aliases: []string{"e"}, Usage: "Event type (created, updated, deleted, all)", Value: "all", }, }, }, }, } } type Task struct { ID string `json:"id"` Title string `json:"title"` Content string `json:"content"` CreatedByID string `json:"createdById"` CreatedBy *User `json:"createdBy"` UpdatedByID string `json:"updatedById"` UpdatedBy *User `json:"updatedBy"` AssigneeID *string `json:"assigneeId"` Assignee *User `json:"assignee"` StatusID *string `json:"statusId"` Status *TaskStatus `json:"status"` DueDate *string `json:"dueDate"` Priority string `json:"priority"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` } type TaskStatus struct { ID string `json:"id"` Code string `json:"code"` Label string `json:"label"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` } func taskList(ctx context.Context, cmd *cli.Command) error { cfg, err := config.Load() if err != nil { return err } if err := RequireAuth(cfg); err != nil { return err } c := client.New(cfg.ServerURL) c.SetToken(cfg.Token) query := "query Tasks { tasks { id title priority createdBy { id email } assignee { id email } status { id code label } dueDate createdAt updatedAt } }" resp, err := c.Query(query, nil) if err != nil { return err } var result struct { Tasks []Task `json:"tasks"` } if err := json.Unmarshal(resp.Data, &result); err != nil { return err } if cmd.Bool("json") { enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") return enc.Encode(result.Tasks) } if len(result.Tasks) == 0 { fmt.Println("No tasks found.") return nil } table := tablewriter.NewWriter(os.Stdout) table.Header([]string{"ID", "Title", "Priority", "Status", "Assignee", "Due Date"}) for _, t := range result.Tasks { status := "" if t.Status != nil { status = t.Status.Label } assignee := "" if t.Assignee != nil { assignee = t.Assignee.Email } dueDate := "" if t.DueDate != nil { dueDate = *t.DueDate } table.Append([]string{t.ID, t.Title, t.Priority, status, assignee, dueDate}) } table.Render() return nil } func taskGet(ctx context.Context, cmd *cli.Command) error { cfg, err := config.Load() if err != nil { return err } if err := RequireAuth(cfg); err != nil { return err } c := client.New(cfg.ServerURL) c.SetToken(cfg.Token) id := cmd.String("id") query := "query Task($id: ID!) { task(id: $id) { id title content createdBy { id email } assignee { id email } status { id code label } dueDate priority createdAt updatedAt } }" resp, err := c.Query(query, map[string]interface{}{"id": id}) if err != nil { return err } var result struct { Task *Task `json:"task"` } if err := json.Unmarshal(resp.Data, &result); err != nil { return err } if result.Task == nil { return fmt.Errorf("task not found") } if cmd.Bool("json") { enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") return enc.Encode(result.Task) } t := result.Task fmt.Printf("ID: %s\n", t.ID) fmt.Printf("Title: %s\n", t.Title) fmt.Printf("Content: %s\n", t.Content) if t.CreatedBy != nil { fmt.Printf("Created By: %s\n", t.CreatedBy.Email) } if t.Assignee != nil { fmt.Printf("Assignee: %s\n", t.Assignee.Email) } if t.Status != nil { fmt.Printf("Status: %s (%s)\n", t.Status.Label, t.Status.Code) } if t.DueDate != nil { fmt.Printf("Due Date: %s\n", *t.DueDate) } fmt.Printf("Priority: %s\n", t.Priority) fmt.Printf("Created At: %s\n", t.CreatedAt) fmt.Printf("Updated At: %s\n", t.UpdatedAt) return nil } func taskCreate(ctx context.Context, cmd *cli.Command) error { cfg, err := config.Load() if err != nil { return err } if err := RequireAuth(cfg); err != nil { return err } title := cmd.String("title") content := cmd.String("content") createdBy := cmd.String("created-by") assignee := cmd.String("assignee") status := cmd.String("status") dueDate := cmd.String("due-date") priority := cmd.String("priority") if title == "" { prompt := &survey.Input{Message: "Title:"} if err := survey.AskOne(prompt, &title, survey.WithValidator(survey.Required)); err != nil { return err } } if content == "" { prompt := &survey.Multiline{Message: "Content:"} if err := survey.AskOne(prompt, &content, survey.WithValidator(survey.Required)); err != nil { return err } } if createdBy == "" { prompt := &survey.Input{Message: "Creator user ID:"} if err := survey.AskOne(prompt, &createdBy, survey.WithValidator(survey.Required)); err != nil { return err } } if priority == "" { prompt := &survey.Select{ Message: "Priority:", Options: []string{"low", "medium", "high"}, Default: "medium", } if err := survey.AskOne(prompt, &priority); err != nil { return err } } // Prompt for assignee (optional) if assignee == "" { prompt := &survey.Input{Message: "Assignee user ID (optional, press Enter to skip):"} if err := survey.AskOne(prompt, &assignee); err != nil { return err } } // Prompt for status (optional) if status == "" { prompt := &survey.Input{Message: "Status ID (optional, press Enter to skip):"} if err := survey.AskOne(prompt, &status); err != nil { return err } } // Prompt for due date (optional) if dueDate == "" { prompt := &survey.Input{Message: "Due date (optional, press Enter to skip):"} if err := survey.AskOne(prompt, &dueDate); err != nil { return err } } c := client.New(cfg.ServerURL) c.SetToken(cfg.Token) mutation := `mutation CreateTask($input: NewTask!) { createTask(input: $input) { id title priority createdBy { id email } assignee { id email } status { id code label } dueDate createdAt updatedAt } }` input := map[string]interface{}{ "title": title, "content": content, "createdById": createdBy, "priority": priority, } if assignee != "" { input["assigneeId"] = assignee } if status != "" { input["statusId"] = status } if dueDate != "" { input["dueDate"] = dueDate } resp, err := c.Mutation(mutation, map[string]interface{}{"input": input}) if err != nil { return err } var result struct { CreateTask *Task `json:"createTask"` } if err := json.Unmarshal(resp.Data, &result); err != nil { return err } if result.CreateTask == nil { return fmt.Errorf("failed to create task") } fmt.Printf("Task created successfully!\n") fmt.Printf("ID: %s\n", result.CreateTask.ID) fmt.Printf("Title: %s\n", result.CreateTask.Title) return nil } func taskUpdate(ctx context.Context, cmd *cli.Command) error { cfg, err := config.Load() if err != nil { return err } if err := RequireAuth(cfg); err != nil { return err } id := cmd.String("id") title := cmd.String("title") content := cmd.String("content") assignee := cmd.String("assignee") status := cmd.String("status") dueDate := cmd.String("due-date") priority := cmd.String("priority") if title == "" && content == "" && assignee == "" && status == "" && dueDate == "" && priority == "" { fmt.Println("No updates provided. Use flags to specify what to update.") return nil } c := client.New(cfg.ServerURL) c.SetToken(cfg.Token) input := make(map[string]interface{}) if title != "" { input["title"] = title } if content != "" { input["content"] = content } if assignee != "" { input["assigneeId"] = assignee } if status != "" { input["statusId"] = status } if dueDate != "" { input["dueDate"] = dueDate } if priority != "" { input["priority"] = priority } mutation := `mutation UpdateTask($id: ID!, $input: UpdateTaskInput!) { updateTask(id: $id, input: $input) { id title priority status { id code label } createdAt updatedAt } }` resp, err := c.Mutation(mutation, map[string]interface{}{"id": id, "input": input}) if err != nil { return err } var result struct { UpdateTask *Task `json:"updateTask"` } if err := json.Unmarshal(resp.Data, &result); err != nil { return err } if result.UpdateTask == nil { return fmt.Errorf("task not found") } fmt.Printf("Task updated successfully!\n") fmt.Printf("ID: %s\n", result.UpdateTask.ID) fmt.Printf("Title: %s\n", result.UpdateTask.Title) return nil } func taskDelete(ctx context.Context, cmd *cli.Command) error { cfg, err := config.Load() if err != nil { return err } if err := RequireAuth(cfg); err != nil { return err } id := cmd.String("id") skipConfirm := cmd.Bool("yes") if !skipConfirm { confirm := false prompt := &survey.Confirm{ Message: fmt.Sprintf("Are you sure you want to delete task %s?", id), Default: false, } if err := survey.AskOne(prompt, &confirm); err != nil { return err } if !confirm { fmt.Println("Deletion cancelled.") return nil } } c := client.New(cfg.ServerURL) c.SetToken(cfg.Token) mutation := `mutation DeleteTask($id: ID!) { deleteTask(id: $id) }` resp, err := c.Mutation(mutation, map[string]interface{}{"id": id}) if err != nil { return err } var result struct { DeleteTask bool `json:"deleteTask"` } if err := json.Unmarshal(resp.Data, &result); err != nil { return err } if result.DeleteTask { fmt.Printf("Task %s deleted successfully.\n", id) } else { fmt.Printf("Failed to delete task %s.\n", id) } return nil } func taskWatch(ctx context.Context, cmd *cli.Command) error { cfg, err := config.Load() if err != nil { return err } if err := RequireAuth(cfg); err != nil { return err } eventType := cmd.String("event") wsClient := client.NewWebSocketClient(cfg.ServerURL, cfg.Token) if err := wsClient.Connect(); err != nil { return fmt.Errorf("failed to connect: %w", err) } defer wsClient.Close() fmt.Printf("Watching for task events (type: %s)...\n", eventType) fmt.Println("Press Ctrl+C to stop.") // GraphQL subscriptions only allow one top-level field per subscription // When watching "all" events, we need to create separate subscriptions switch eventType { case "created": subscription := "subscription { taskCreated { id title priority status { id code label } createdAt } }" if err := wsClient.Subscribe("1", subscription, nil); err != nil { return fmt.Errorf("failed to subscribe: %w", err) } case "updated": subscription := "subscription { taskUpdated { id title priority status { id code label } updatedAt } }" if err := wsClient.Subscribe("1", subscription, nil); err != nil { return fmt.Errorf("failed to subscribe: %w", err) } case "deleted": subscription := "subscription { taskDeleted { id title } }" if err := wsClient.Subscribe("1", subscription, nil); err != nil { return fmt.Errorf("failed to subscribe: %w", err) } default: // Subscribe to all three event types with separate subscriptions subscriptions := []struct { id string query string }{ {"1", "subscription { taskCreated { id title priority status { id code label } createdAt } }"}, {"2", "subscription { taskUpdated { id title priority status { id code label } updatedAt } }"}, {"3", "subscription { taskDeleted { id title } }"}, } for _, sub := range subscriptions { if err := wsClient.Subscribe(sub.id, sub.query, nil); err != nil { return fmt.Errorf("failed to subscribe to %s: %w", sub.id, err) } } } for { select { case msg := <-wsClient.Messages(): fmt.Printf("Event: %s\n", string(msg)) case err := <-wsClient.Errors(): fmt.Fprintf(os.Stderr, "Error: %v\n", err) case <-wsClient.Done(): return nil case <-ctx.Done(): return nil } } }