Files
HarborForge.Cli/internal/commands/task.go

484 lines
12 KiB
Go

package commands
import (
"bytes"
"encoding/json"
"fmt"
"git.hangman-lab.top/zhi/HarborForge.Cli/internal/client"
"git.hangman-lab.top/zhi/HarborForge.Cli/internal/config"
"git.hangman-lab.top/zhi/HarborForge.Cli/internal/output"
)
// taskResponse matches the backend TaskResponse schema.
type taskResponse struct {
ID int `json:"id"`
Code string `json:"code"`
Title string `json:"title"`
Description *string `json:"description"`
Status string `json:"status"`
Priority string `json:"priority"`
Type string `json:"type"`
DueDate *string `json:"due_date"`
ProjectCode string `json:"project_code"`
MilestoneCode *string `json:"milestone_code"`
TakenBy *string `json:"taken_by"`
CreatedAt string `json:"created_at"`
}
// RunTaskList implements `hf task list`.
func RunTaskList(args []string, tokenFlag string) {
token := ResolveToken(tokenFlag)
query := ""
for i := 0; i < len(args); i++ {
switch args[i] {
case "--project":
if i+1 >= len(args) {
output.Error("--project requires a value")
}
i++
query = appendQuery(query, "project_code", args[i])
case "--milestone":
if i+1 >= len(args) {
output.Error("--milestone requires a value")
}
i++
query = appendQuery(query, "milestone_code", args[i])
case "--status":
if i+1 >= len(args) {
output.Error("--status requires a value")
}
i++
query = appendQuery(query, "status", args[i])
case "--taken-by":
if i+1 >= len(args) {
output.Error("--taken-by requires a value")
}
i++
query = appendQuery(query, "taken_by", args[i])
case "--due-today":
if i+1 >= len(args) {
output.Error("--due-today requires a value")
}
i++
query = appendQuery(query, "due_today", args[i])
case "--order-by":
if i+1 >= len(args) {
output.Error("--order-by requires a value")
}
i++
query = appendQuery(query, "order_by", args[i])
default:
output.Errorf("unknown flag: %s", args[i])
}
}
cfg, err := config.Load()
if err != nil {
output.Errorf("config error: %v", err)
}
c := client.New(cfg.BaseURL, token)
path := "/tasks"
if query != "" {
path += "?" + query
}
data, err := c.Get(path)
if err != nil {
output.Errorf("failed to list tasks: %v", err)
}
if output.JSONMode {
var raw json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
output.Errorf("invalid JSON response: %v", err)
}
output.PrintJSON(raw)
return
}
var tasks []taskResponse
if err := json.Unmarshal(data, &tasks); err != nil {
output.Errorf("cannot parse task list: %v", err)
}
headers := []string{"CODE", "TITLE", "STATUS", "PRIORITY", "TAKEN BY", "PROJECT"}
var rows [][]string
for _, t := range tasks {
takenBy := ""
if t.TakenBy != nil {
takenBy = *t.TakenBy
}
title := t.Title
if len(title) > 40 {
title = title[:37] + "..."
}
rows = append(rows, []string{t.Code, title, t.Status, t.Priority, takenBy, t.ProjectCode})
}
output.PrintTable(headers, rows)
}
// RunTaskGet implements `hf task get <task-code>`.
func RunTaskGet(taskCode, tokenFlag string) {
token := ResolveToken(tokenFlag)
cfg, err := config.Load()
if err != nil {
output.Errorf("config error: %v", err)
}
c := client.New(cfg.BaseURL, token)
data, err := c.Get("/tasks/" + taskCode)
if err != nil {
output.Errorf("failed to get task: %v", err)
}
if output.JSONMode {
var raw json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
output.Errorf("invalid JSON response: %v", err)
}
output.PrintJSON(raw)
return
}
var t taskResponse
if err := json.Unmarshal(data, &t); err != nil {
output.Errorf("cannot parse task: %v", err)
}
desc := ""
if t.Description != nil {
desc = *t.Description
}
due := ""
if t.DueDate != nil {
due = *t.DueDate
}
milestone := ""
if t.MilestoneCode != nil {
milestone = *t.MilestoneCode
}
takenBy := ""
if t.TakenBy != nil {
takenBy = *t.TakenBy
}
output.PrintKeyValue(
"code", t.Code,
"title", t.Title,
"description", desc,
"status", t.Status,
"priority", t.Priority,
"type", t.Type,
"due-date", due,
"project", t.ProjectCode,
"milestone", milestone,
"taken-by", takenBy,
"created", t.CreatedAt,
)
}
// RunTaskCreate implements `hf task create`.
func RunTaskCreate(args []string, tokenFlag string) {
token := ResolveToken(tokenFlag)
project, title, milestone, taskType, priority, desc := "", "", "", "", "", ""
for i := 0; i < len(args); i++ {
switch args[i] {
case "--project":
if i+1 >= len(args) {
output.Error("--project requires a value")
}
i++
project = args[i]
case "--title":
if i+1 >= len(args) {
output.Error("--title requires a value")
}
i++
title = args[i]
case "--milestone":
if i+1 >= len(args) {
output.Error("--milestone requires a value")
}
i++
milestone = args[i]
case "--type":
if i+1 >= len(args) {
output.Error("--type requires a value")
}
i++
taskType = args[i]
case "--priority":
if i+1 >= len(args) {
output.Error("--priority requires a value")
}
i++
priority = args[i]
case "--desc":
if i+1 >= len(args) {
output.Error("--desc requires a value")
}
i++
desc = args[i]
default:
output.Errorf("unknown flag: %s", args[i])
}
}
if project == "" || title == "" {
output.Error("usage: hf task create --project <project-code> --title <title>")
}
// story/* types are restricted — must be created via `hf proposal accept`
if taskType == "story" || (len(taskType) > 6 && taskType[:6] == "story/") {
output.Error("story tasks are restricted and cannot be created directly.\nUse 'hf proposal accept <proposal-code> --milestone <milestone-code>' to generate story tasks from a proposal.")
}
payload := map[string]interface{}{
"project_code": project,
"title": title,
}
if milestone != "" {
payload["milestone_code"] = milestone
}
if taskType != "" {
payload["type"] = taskType
}
if priority != "" {
payload["priority"] = priority
}
if desc != "" {
payload["description"] = desc
}
body, err := json.Marshal(payload)
if err != nil {
output.Errorf("cannot marshal payload: %v", err)
}
cfg, err := config.Load()
if err != nil {
output.Errorf("config error: %v", err)
}
c := client.New(cfg.BaseURL, token)
data, err := c.Post("/tasks", bytes.NewReader(body))
if err != nil {
output.Errorf("failed to create task: %v", err)
}
if output.JSONMode {
var raw json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
output.Errorf("invalid JSON response: %v", err)
}
output.PrintJSON(raw)
return
}
var t taskResponse
if err := json.Unmarshal(data, &t); err != nil {
fmt.Printf("task created: %s\n", title)
return
}
fmt.Printf("task created: %s (code: %s)\n", t.Title, t.Code)
}
// RunTaskUpdate implements `hf task update <task-code>`.
func RunTaskUpdate(taskCode string, args []string, tokenFlag string) {
token := ResolveToken(tokenFlag)
payload := make(map[string]interface{})
for i := 0; i < len(args); i++ {
switch args[i] {
case "--title":
if i+1 >= len(args) {
output.Error("--title requires a value")
}
i++
payload["title"] = args[i]
case "--desc":
if i+1 >= len(args) {
output.Error("--desc requires a value")
}
i++
payload["description"] = args[i]
case "--status":
if i+1 >= len(args) {
output.Error("--status requires a value")
}
i++
payload["status"] = args[i]
case "--priority":
if i+1 >= len(args) {
output.Error("--priority requires a value")
}
i++
payload["priority"] = args[i]
case "--assignee":
if i+1 >= len(args) {
output.Error("--assignee requires a value")
}
i++
val := args[i]
if val == "null" {
payload["taken_by"] = nil
} else {
payload["taken_by"] = val
}
default:
output.Errorf("unknown flag: %s", args[i])
}
}
if len(payload) == 0 {
output.Error("nothing to update — provide at least one flag")
}
body, err := json.Marshal(payload)
if err != nil {
output.Errorf("cannot marshal payload: %v", err)
}
cfg, err := config.Load()
if err != nil {
output.Errorf("config error: %v", err)
}
c := client.New(cfg.BaseURL, token)
_, err = c.Patch("/tasks/"+taskCode, bytes.NewReader(body))
if err != nil {
output.Errorf("failed to update task: %v", err)
}
fmt.Printf("task updated: %s\n", taskCode)
}
// RunTaskTransition implements `hf task transition <task-code> <status>`.
func RunTaskTransition(taskCode, status, tokenFlag string) {
token := ResolveToken(tokenFlag)
payload := map[string]interface{}{
"status": status,
}
body, err := json.Marshal(payload)
if err != nil {
output.Errorf("cannot marshal payload: %v", err)
}
cfg, err := config.Load()
if err != nil {
output.Errorf("config error: %v", err)
}
c := client.New(cfg.BaseURL, token)
_, err = c.Post("/tasks/"+taskCode+"/transition", bytes.NewReader(body))
if err != nil {
output.Errorf("failed to transition task: %v", err)
}
fmt.Printf("task %s transitioned to %s\n", taskCode, status)
}
// RunTaskTake implements `hf task take <task-code>`.
func RunTaskTake(taskCode, tokenFlag string) {
token := ResolveToken(tokenFlag)
cfg, err := config.Load()
if err != nil {
output.Errorf("config error: %v", err)
}
c := client.New(cfg.BaseURL, token)
_, err = c.Post("/tasks/"+taskCode+"/take", nil)
if err != nil {
output.Errorf("failed to take task: %v", err)
}
fmt.Printf("task taken: %s\n", taskCode)
}
// RunTaskDelete implements `hf task delete <task-code>`.
func RunTaskDelete(taskCode, tokenFlag string) {
token := ResolveToken(tokenFlag)
cfg, err := config.Load()
if err != nil {
output.Errorf("config error: %v", err)
}
c := client.New(cfg.BaseURL, token)
_, err = c.Delete("/tasks/" + taskCode)
if err != nil {
output.Errorf("failed to delete task: %v", err)
}
fmt.Printf("task deleted: %s\n", taskCode)
}
// RunTaskSearch implements `hf task search`.
func RunTaskSearch(args []string, tokenFlag string) {
token := ResolveToken(tokenFlag)
query := ""
for i := 0; i < len(args); i++ {
switch args[i] {
case "--query":
if i+1 >= len(args) {
output.Error("--query requires a value")
}
i++
query = appendQuery(query, "q", args[i])
case "--project":
if i+1 >= len(args) {
output.Error("--project requires a value")
}
i++
query = appendQuery(query, "project_code", args[i])
case "--status":
if i+1 >= len(args) {
output.Error("--status requires a value")
}
i++
query = appendQuery(query, "status", args[i])
default:
output.Errorf("unknown flag: %s", args[i])
}
}
cfg, err := config.Load()
if err != nil {
output.Errorf("config error: %v", err)
}
c := client.New(cfg.BaseURL, token)
path := "/tasks/search"
if query != "" {
path += "?" + query
}
data, err := c.Get(path)
if err != nil {
output.Errorf("failed to search tasks: %v", err)
}
if output.JSONMode {
var raw json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
output.Errorf("invalid JSON response: %v", err)
}
output.PrintJSON(raw)
return
}
var tasks []taskResponse
if err := json.Unmarshal(data, &tasks); err != nil {
output.Errorf("cannot parse task list: %v", err)
}
headers := []string{"CODE", "TITLE", "STATUS", "PRIORITY", "TAKEN BY", "PROJECT"}
var rows [][]string
for _, t := range tasks {
takenBy := ""
if t.TakenBy != nil {
takenBy = *t.TakenBy
}
title := t.Title
if len(title) > 40 {
title = title[:37] + "..."
}
rows = append(rows, []string{t.Code, title, t.Status, t.Priority, takenBy, t.ProjectCode})
}
output.PrintTable(headers, rows)
}