CLI-CAL-001/002/003/004/005/006/007/008/009/010: Add calendar command group and CRUD plan commands
- Add hf calendar command group to command surface and router - Implement schedule/show/edit/cancel/date-list commands - Implement plan-schedule/plan-list/plan-edit/plan-cancel commands - Add leaf help for all calendar commands - Align CLI with backend calendar routes and response envelopes - Support virtual slot ids for edit/cancel - Validate with go build and go test ./...
This commit is contained in:
@@ -190,6 +190,9 @@ func handleGroup(group help.Group, args []string) {
|
||||
case "proposal", "propose":
|
||||
handleProposalCommand(sub.Name, remaining)
|
||||
return
|
||||
case "calendar":
|
||||
handleCalendarCommand(sub.Name, remaining)
|
||||
return
|
||||
case "comment":
|
||||
handleCommentCommand(sub.Name, remaining)
|
||||
return
|
||||
@@ -700,6 +703,45 @@ func handleSupportCommand(subCmd string, args []string) {
|
||||
}
|
||||
}
|
||||
|
||||
func handleCalendarCommand(subCmd string, args []string) {
|
||||
tokenFlag := ""
|
||||
var filtered []string
|
||||
for i := 0; i < len(args); i++ {
|
||||
switch args[i] {
|
||||
case "--token":
|
||||
if i+1 < len(args) {
|
||||
i++
|
||||
tokenFlag = args[i]
|
||||
}
|
||||
default:
|
||||
filtered = append(filtered, args[i])
|
||||
}
|
||||
}
|
||||
|
||||
switch subCmd {
|
||||
case "schedule":
|
||||
commands.RunCalendarSchedule(filtered, tokenFlag)
|
||||
case "show":
|
||||
commands.RunCalendarShow(filtered, tokenFlag)
|
||||
case "edit":
|
||||
commands.RunCalendarEdit(filtered, tokenFlag)
|
||||
case "cancel":
|
||||
commands.RunCalendarCancel(filtered, tokenFlag)
|
||||
case "date-list":
|
||||
commands.RunCalendarDateList(filtered, tokenFlag)
|
||||
case "plan-schedule":
|
||||
commands.RunCalendarPlanSchedule(filtered, tokenFlag)
|
||||
case "plan-list":
|
||||
commands.RunCalendarPlanList(filtered, tokenFlag)
|
||||
case "plan-edit":
|
||||
commands.RunCalendarPlanEdit(filtered, tokenFlag)
|
||||
case "plan-cancel":
|
||||
commands.RunCalendarPlanCancel(filtered, tokenFlag)
|
||||
default:
|
||||
output.Errorf("hf calendar %s is not implemented yet", subCmd)
|
||||
}
|
||||
}
|
||||
|
||||
func handleProposalCommand(subCmd string, args []string) {
|
||||
tokenFlag := ""
|
||||
var filtered []string
|
||||
|
||||
660
internal/commands/calendar.go
Normal file
660
internal/commands/calendar.go
Normal file
@@ -0,0 +1,660 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// --- Slot Commands ---
|
||||
|
||||
// RunCalendarSchedule implements `hf calendar schedule <slot-type> <scheduled-at> <estimated-duration> [--job <code>] [--date <yyyy-mm-dd>]`.
|
||||
func RunCalendarSchedule(args []string, tokenFlag string) {
|
||||
token := ResolveToken(tokenFlag)
|
||||
|
||||
if len(args) < 3 {
|
||||
output.Error("usage: hf calendar schedule <slot-type> <scheduled-at> <estimated-duration> [--job <code>] [--date <yyyy-mm-dd>] [--priority <0-99>]")
|
||||
}
|
||||
|
||||
slotType := args[0]
|
||||
scheduledAt := args[1]
|
||||
estimatedDuration := args[2]
|
||||
|
||||
date, jobCode, priority := "", "", ""
|
||||
remaining := args[3:]
|
||||
for i := 0; i < len(remaining); i++ {
|
||||
switch remaining[i] {
|
||||
case "--date":
|
||||
if i+1 >= len(remaining) {
|
||||
output.Error("--date requires a value")
|
||||
}
|
||||
i++
|
||||
date = remaining[i]
|
||||
case "--job":
|
||||
if i+1 >= len(remaining) {
|
||||
output.Error("--job requires a value")
|
||||
}
|
||||
i++
|
||||
jobCode = remaining[i]
|
||||
case "--priority":
|
||||
if i+1 >= len(remaining) {
|
||||
output.Error("--priority requires a value")
|
||||
}
|
||||
i++
|
||||
priority = remaining[i]
|
||||
default:
|
||||
output.Errorf("unknown flag: %s", remaining[i])
|
||||
}
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"slot_type": slotType,
|
||||
"scheduled_at": scheduledAt,
|
||||
"estimated_duration": estimatedDuration,
|
||||
}
|
||||
if date != "" {
|
||||
payload["date"] = date
|
||||
}
|
||||
if priority != "" {
|
||||
payload["priority"] = priority
|
||||
}
|
||||
if jobCode != "" {
|
||||
payload["event_type"] = "job"
|
||||
payload["event_data"] = map[string]interface{}{"type": "Task", "code": jobCode}
|
||||
}
|
||||
|
||||
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("/calendar/slots", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
output.Errorf("failed to schedule slot: %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
|
||||
}
|
||||
|
||||
// Check for warnings
|
||||
var resp map[string]interface{}
|
||||
if err := json.Unmarshal(data, &resp); err == nil {
|
||||
if ws, ok := resp["warnings"]; ok {
|
||||
if warnings, ok := ws.([]interface{}); ok && len(warnings) > 0 {
|
||||
fmt.Println("⚠️ Workload warnings:")
|
||||
for _, w := range warnings {
|
||||
if wm, ok := w.(map[string]interface{}); ok {
|
||||
if msg, ok := wm["message"].(string); ok {
|
||||
fmt.Printf(" - %s\n", msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("slot scheduled: %s at %s (%s min)\n", slotType, scheduledAt, estimatedDuration)
|
||||
}
|
||||
|
||||
// RunCalendarShow implements `hf calendar show [--date <yyyy-mm-dd>]`.
|
||||
func RunCalendarShow(args []string, tokenFlag string) {
|
||||
token := ResolveToken(tokenFlag)
|
||||
|
||||
date := ""
|
||||
for i := 0; i < len(args); i++ {
|
||||
switch args[i] {
|
||||
case "--date":
|
||||
if i+1 >= len(args) {
|
||||
output.Error("--date requires a value")
|
||||
}
|
||||
i++
|
||||
date = 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 := "/calendar/day"
|
||||
if date != "" {
|
||||
path += "?date=" + date
|
||||
}
|
||||
|
||||
data, err := c.Get(path)
|
||||
if err != nil {
|
||||
output.Errorf("failed to show calendar: %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 resp struct {
|
||||
Slots []map[string]interface{} `json:"slots"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
output.Errorf("cannot parse calendar: %v", err)
|
||||
}
|
||||
slots := resp.Slots
|
||||
|
||||
if len(slots) == 0 {
|
||||
if date != "" {
|
||||
fmt.Printf("No slots for %s\n", date)
|
||||
} else {
|
||||
fmt.Println("No slots for today")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
headers := []string{"ID", "TIME", "TYPE", "DURATION", "PRIORITY", "STATUS", "EVENT"}
|
||||
var rows [][]string
|
||||
for _, s := range slots {
|
||||
slotID := fmt.Sprintf("%v", s["slot_id"])
|
||||
scheduled := fmt.Sprintf("%v", s["scheduled_at"])
|
||||
slotType := fmt.Sprintf("%v", s["slot_type"])
|
||||
dur := fmt.Sprintf("%v min", s["estimated_duration"])
|
||||
pri := fmt.Sprintf("%v", s["priority"])
|
||||
status := fmt.Sprintf("%v", s["status"])
|
||||
event := ""
|
||||
if et, ok := s["event_type"]; ok && et != nil {
|
||||
event = fmt.Sprintf("%v", et)
|
||||
}
|
||||
if ed, ok := s["event_data"]; ok && ed != nil {
|
||||
if edm, ok := ed.(map[string]interface{}); ok {
|
||||
if code, ok := edm["code"]; ok {
|
||||
event += " " + fmt.Sprintf("%v", code)
|
||||
}
|
||||
if ev, ok := edm["event"]; ok {
|
||||
event += " " + fmt.Sprintf("%v", ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
if isVirt, ok := s["is_virtual"]; ok && isVirt == true {
|
||||
slotID += " (plan)"
|
||||
}
|
||||
rows = append(rows, []string{slotID, scheduled, slotType, dur, pri, status, strings.TrimSpace(event)})
|
||||
}
|
||||
output.PrintTable(headers, rows)
|
||||
}
|
||||
|
||||
// RunCalendarEdit implements `hf calendar edit [--date <yyyy-mm-dd>] <slot-id> [flags]`.
|
||||
func RunCalendarEdit(args []string, tokenFlag string) {
|
||||
token := ResolveToken(tokenFlag)
|
||||
|
||||
if len(args) < 1 {
|
||||
output.Error("usage: hf calendar edit <slot-id> [--date <yyyy-mm-dd>] [--slot-type <type>] [--estimated-duration <mins>] [--scheduled-at <HH:mm>] [--job <code>]")
|
||||
}
|
||||
|
||||
slotID := args[0]
|
||||
date, slotType, duration, scheduledAt, jobCode := "", "", "", "", ""
|
||||
|
||||
for i := 1; i < len(args); i++ {
|
||||
switch args[i] {
|
||||
case "--date":
|
||||
if i+1 >= len(args) {
|
||||
output.Error("--date requires a value")
|
||||
}
|
||||
i++
|
||||
date = args[i]
|
||||
case "--slot-type":
|
||||
if i+1 >= len(args) {
|
||||
output.Error("--slot-type requires a value")
|
||||
}
|
||||
i++
|
||||
slotType = args[i]
|
||||
case "--estimated-duration":
|
||||
if i+1 >= len(args) {
|
||||
output.Error("--estimated-duration requires a value")
|
||||
}
|
||||
i++
|
||||
duration = args[i]
|
||||
case "--scheduled-at":
|
||||
if i+1 >= len(args) {
|
||||
output.Error("--scheduled-at requires a value")
|
||||
}
|
||||
i++
|
||||
scheduledAt = args[i]
|
||||
case "--job":
|
||||
if i+1 >= len(args) {
|
||||
output.Error("--job requires a value")
|
||||
}
|
||||
i++
|
||||
jobCode = args[i]
|
||||
default:
|
||||
output.Errorf("unknown flag: %s", args[i])
|
||||
}
|
||||
}
|
||||
|
||||
payload := make(map[string]interface{})
|
||||
if slotType != "" {
|
||||
payload["slot_type"] = slotType
|
||||
}
|
||||
if duration != "" {
|
||||
payload["estimated_duration"] = duration
|
||||
}
|
||||
if scheduledAt != "" {
|
||||
payload["scheduled_at"] = scheduledAt
|
||||
}
|
||||
if jobCode != "" {
|
||||
payload["event_type"] = "job"
|
||||
payload["event_data"] = map[string]interface{}{"type": "Task", "code": jobCode}
|
||||
}
|
||||
|
||||
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)
|
||||
path := "/calendar/slots/" + slotID
|
||||
if strings.HasPrefix(slotID, "plan-") {
|
||||
path = "/calendar/slots/virtual/" + slotID
|
||||
}
|
||||
_ = date // kept for CLI compatibility; backend identifies virtual slots via slot-id
|
||||
data, err := c.Patch(path, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
output.Errorf("failed to edit slot: %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 resp map[string]interface{}
|
||||
if err := json.Unmarshal(data, &resp); err == nil {
|
||||
if ws, ok := resp["warnings"]; ok {
|
||||
if warnings, ok := ws.([]interface{}); ok && len(warnings) > 0 {
|
||||
fmt.Println("⚠️ Workload warnings:")
|
||||
for _, w := range warnings {
|
||||
if wm, ok := w.(map[string]interface{}); ok {
|
||||
if msg, ok := wm["message"].(string); ok {
|
||||
fmt.Printf(" - %s\n", msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("slot edited: %s\n", slotID)
|
||||
}
|
||||
|
||||
// RunCalendarCancel implements `hf calendar cancel [--date <yyyy-mm-dd>] <slot-id>`.
|
||||
func RunCalendarCancel(args []string, tokenFlag string) {
|
||||
token := ResolveToken(tokenFlag)
|
||||
|
||||
if len(args) < 1 {
|
||||
output.Error("usage: hf calendar cancel <slot-id> [--date <yyyy-mm-dd>]")
|
||||
}
|
||||
|
||||
slotID := args[0]
|
||||
date := ""
|
||||
for i := 1; i < len(args); i++ {
|
||||
switch args[i] {
|
||||
case "--date":
|
||||
if i+1 >= len(args) {
|
||||
output.Error("--date requires a value")
|
||||
}
|
||||
i++
|
||||
date = 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 := "/calendar/slots/" + slotID + "/cancel"
|
||||
if strings.HasPrefix(slotID, "plan-") {
|
||||
path = "/calendar/slots/virtual/" + slotID + "/cancel"
|
||||
}
|
||||
_ = date // kept for CLI compatibility; backend identifies virtual slots via slot-id
|
||||
_, err = c.Post(path, nil)
|
||||
if err != nil {
|
||||
output.Errorf("failed to cancel slot: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("slot cancelled: %s\n", slotID)
|
||||
}
|
||||
|
||||
// RunCalendarDateList implements `hf calendar date-list`.
|
||||
func RunCalendarDateList(args []string, 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("/calendar/dates")
|
||||
if err != nil {
|
||||
output.Errorf("failed to list dates: %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 resp struct {
|
||||
Dates []string `json:"dates"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
output.Errorf("cannot parse dates: %v", err)
|
||||
}
|
||||
dates := resp.Dates
|
||||
|
||||
if len(dates) == 0 {
|
||||
fmt.Println("No future dates with materialized slots")
|
||||
return
|
||||
}
|
||||
|
||||
for _, d := range dates {
|
||||
fmt.Println(d)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Plan Commands ---
|
||||
|
||||
// RunCalendarPlanSchedule implements `hf calendar plan-schedule <slot-type> <estimated-duration> --at <HH:mm> [--on-day <day>] [--on-week <1-4>] [--on-month <month>]`.
|
||||
func RunCalendarPlanSchedule(args []string, tokenFlag string) {
|
||||
token := ResolveToken(tokenFlag)
|
||||
|
||||
if len(args) < 2 {
|
||||
output.Error("usage: hf calendar plan-schedule <slot-type> <estimated-duration> --at <HH:mm> [--on-day <day>] [--on-week <1-4>] [--on-month <month>]")
|
||||
}
|
||||
|
||||
slotType := args[0]
|
||||
duration := args[1]
|
||||
atTime, onDay, onWeek, onMonth := "", "", "", ""
|
||||
|
||||
for i := 2; i < len(args); i++ {
|
||||
switch args[i] {
|
||||
case "--at":
|
||||
if i+1 >= len(args) {
|
||||
output.Error("--at requires a value")
|
||||
}
|
||||
i++
|
||||
atTime = args[i]
|
||||
case "--on-day":
|
||||
if i+1 >= len(args) {
|
||||
output.Error("--on-day requires a value")
|
||||
}
|
||||
i++
|
||||
onDay = args[i]
|
||||
case "--on-week":
|
||||
if i+1 >= len(args) {
|
||||
output.Error("--on-week requires a value")
|
||||
}
|
||||
i++
|
||||
onWeek = args[i]
|
||||
case "--on-month":
|
||||
if i+1 >= len(args) {
|
||||
output.Error("--on-month requires a value")
|
||||
}
|
||||
i++
|
||||
onMonth = args[i]
|
||||
default:
|
||||
output.Errorf("unknown flag: %s", args[i])
|
||||
}
|
||||
}
|
||||
|
||||
if atTime == "" {
|
||||
output.Error("--at is required")
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"slot_type": slotType,
|
||||
"estimated_duration": duration,
|
||||
"at_time": atTime,
|
||||
}
|
||||
if onDay != "" {
|
||||
payload["on_day"] = onDay
|
||||
}
|
||||
if onWeek != "" {
|
||||
payload["on_week"] = onWeek
|
||||
}
|
||||
if onMonth != "" {
|
||||
payload["on_month"] = onMonth
|
||||
}
|
||||
|
||||
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("/calendar/plans", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
output.Errorf("failed to create plan: %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
|
||||
}
|
||||
|
||||
fmt.Printf("plan created: %s at %s (%s min)\n", slotType, atTime, duration)
|
||||
}
|
||||
|
||||
// RunCalendarPlanList implements `hf calendar plan-list`.
|
||||
func RunCalendarPlanList(args []string, 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("/calendar/plans")
|
||||
if err != nil {
|
||||
output.Errorf("failed to list plans: %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 resp struct {
|
||||
Plans []map[string]interface{} `json:"plans"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
output.Errorf("cannot parse plans: %v", err)
|
||||
}
|
||||
plans := resp.Plans
|
||||
|
||||
if len(plans) == 0 {
|
||||
fmt.Println("No schedule plans")
|
||||
return
|
||||
}
|
||||
|
||||
headers := []string{"ID", "TYPE", "AT", "ON DAY", "ON WEEK", "ON MONTH", "DURATION", "ACTIVE"}
|
||||
var rows [][]string
|
||||
for _, p := range plans {
|
||||
id := fmt.Sprintf("%v", p["id"])
|
||||
slotType := fmt.Sprintf("%v", p["slot_type"])
|
||||
at := fmt.Sprintf("%v", p["at_time"])
|
||||
onDay := ""
|
||||
if d, ok := p["on_day"]; ok && d != nil {
|
||||
onDay = fmt.Sprintf("%v", d)
|
||||
}
|
||||
onWeek := ""
|
||||
if w, ok := p["on_week"]; ok && w != nil {
|
||||
onWeek = fmt.Sprintf("%v", w)
|
||||
}
|
||||
onMonth := ""
|
||||
if m, ok := p["on_month"]; ok && m != nil {
|
||||
onMonth = fmt.Sprintf("%v", m)
|
||||
}
|
||||
dur := fmt.Sprintf("%v min", p["estimated_duration"])
|
||||
active := "yes"
|
||||
if a, ok := p["is_active"]; ok && a == false {
|
||||
active = "no"
|
||||
}
|
||||
rows = append(rows, []string{id, slotType, at, onDay, onWeek, onMonth, dur, active})
|
||||
}
|
||||
output.PrintTable(headers, rows)
|
||||
}
|
||||
|
||||
// RunCalendarPlanEdit implements `hf calendar plan-edit <plan-id> [flags]`.
|
||||
func RunCalendarPlanEdit(args []string, tokenFlag string) {
|
||||
token := ResolveToken(tokenFlag)
|
||||
|
||||
if len(args) < 1 {
|
||||
output.Error("usage: hf calendar plan-edit <plan-id> [--at <HH:mm>] [--on-day <day>] [--on-week <1-4>] [--on-month <month>] [--slot-type <type>] [--estimated-duration <mins>]")
|
||||
}
|
||||
|
||||
planID := args[0]
|
||||
payload := make(map[string]interface{})
|
||||
|
||||
for i := 1; i < len(args); i++ {
|
||||
switch args[i] {
|
||||
case "--at":
|
||||
if i+1 >= len(args) {
|
||||
output.Error("--at requires a value")
|
||||
}
|
||||
i++
|
||||
payload["at_time"] = args[i]
|
||||
case "--on-day":
|
||||
if i+1 >= len(args) {
|
||||
output.Error("--on-day requires a value")
|
||||
}
|
||||
i++
|
||||
payload["on_day"] = args[i]
|
||||
case "--on-week":
|
||||
if i+1 >= len(args) {
|
||||
output.Error("--on-week requires a value")
|
||||
}
|
||||
i++
|
||||
payload["on_week"] = args[i]
|
||||
case "--on-month":
|
||||
if i+1 >= len(args) {
|
||||
output.Error("--on-month requires a value")
|
||||
}
|
||||
i++
|
||||
payload["on_month"] = args[i]
|
||||
case "--slot-type":
|
||||
if i+1 >= len(args) {
|
||||
output.Error("--slot-type requires a value")
|
||||
}
|
||||
i++
|
||||
payload["slot_type"] = args[i]
|
||||
case "--estimated-duration":
|
||||
if i+1 >= len(args) {
|
||||
output.Error("--estimated-duration requires a value")
|
||||
}
|
||||
i++
|
||||
payload["estimated_duration"] = args[i]
|
||||
default:
|
||||
output.Errorf("unknown flag: %s", args[i])
|
||||
}
|
||||
}
|
||||
|
||||
if len(payload) == 0 {
|
||||
output.Error("nothing to edit — 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)
|
||||
data, err := c.Patch("/calendar/plans/"+planID, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
output.Errorf("failed to edit plan: %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
|
||||
}
|
||||
|
||||
fmt.Printf("plan edited: %s\n", planID)
|
||||
}
|
||||
|
||||
// RunCalendarPlanCancel implements `hf calendar plan-cancel <plan-id>`.
|
||||
func RunCalendarPlanCancel(args []string, tokenFlag string) {
|
||||
token := ResolveToken(tokenFlag)
|
||||
|
||||
if len(args) < 1 {
|
||||
output.Error("usage: hf calendar plan-cancel <plan-id>")
|
||||
}
|
||||
|
||||
planID := args[0]
|
||||
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
output.Errorf("config error: %v", err)
|
||||
}
|
||||
c := client.New(cfg.BaseURL, token)
|
||||
_, err = c.Post("/calendar/plans/"+planID+"/cancel", nil)
|
||||
if err != nil {
|
||||
output.Errorf("failed to cancel plan: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("plan cancelled: %s\n", planID)
|
||||
}
|
||||
@@ -170,6 +170,15 @@ func leafHelpSpec(group, cmd string) (leafHelp, bool) {
|
||||
"proposal/essential/create": {Summary: "Create an essential", Usage: []string{"hf proposal essential create --proposal <proposal-code> --title <title> --type <feature|improvement|refactor> [--desc <desc>]"}, Flags: authFlagHelp()},
|
||||
"proposal/essential/update": {Summary: "Update an essential", Usage: []string{"hf proposal essential update <essential-code> --proposal <proposal-code> [--title <title>] [--type <type>] [--desc <desc>]"}, Flags: authFlagHelp()},
|
||||
"proposal/essential/delete": {Summary: "Delete an essential", Usage: []string{"hf proposal essential delete <essential-code> --proposal <proposal-code>"}, Flags: authFlagHelp()},
|
||||
"calendar/schedule": {Summary: "Create a one-off calendar slot", Usage: []string{"hf calendar schedule <slot-type> <scheduled-at> <estimated-duration> [--job <code>] [--date <yyyy-mm-dd>] [--priority <0-99>]"}, Flags: authFlagHelp()},
|
||||
"calendar/show": {Summary: "Show slots for a day", Usage: []string{"hf calendar show [--date <yyyy-mm-dd>]", "hf calendar show --json [--date <yyyy-mm-dd>]"}, Flags: authFlagHelp()},
|
||||
"calendar/edit": {Summary: "Edit a calendar slot", Usage: []string{"hf calendar edit <slot-id> [--date <yyyy-mm-dd>] [--slot-type <type>] [--estimated-duration <mins>] [--job <code>] [--scheduled-at <HH:mm>]"}, Flags: authFlagHelp(), Notes: []string{"For virtual plan slots, pass the virtual id like plan-12-2026-04-01."}},
|
||||
"calendar/cancel": {Summary: "Cancel a calendar slot", Usage: []string{"hf calendar cancel <slot-id> [--date <yyyy-mm-dd>]"}, Flags: authFlagHelp()},
|
||||
"calendar/date-list": {Summary: "List future dates with materialized slots", Usage: []string{"hf calendar date-list", "hf calendar date-list --json"}, Flags: authFlagHelp()},
|
||||
"calendar/plan-schedule": {Summary: "Create a recurring schedule plan", Usage: []string{"hf calendar plan-schedule <slot-type> <estimated-duration> --at <HH:mm> [--on-day <day>] [--on-week <1-4>] [--on-month <month>]"}, Flags: authFlagHelp()},
|
||||
"calendar/plan-list": {Summary: "List recurring schedule plans", Usage: []string{"hf calendar plan-list", "hf calendar plan-list --json"}, Flags: authFlagHelp()},
|
||||
"calendar/plan-edit": {Summary: "Edit a recurring schedule plan", Usage: []string{"hf calendar plan-edit <plan-id> [--at <HH:mm>] [--on-day <day>] [--on-week <1-4>] [--on-month <month>] [--slot-type <type>] [--estimated-duration <mins>]"}, Flags: authFlagHelp()},
|
||||
"calendar/plan-cancel": {Summary: "Cancel a recurring schedule plan", Usage: []string{"hf calendar plan-cancel <plan-id>"}, Flags: authFlagHelp()},
|
||||
"comment/add": {Summary: "Add a comment to a task", Usage: []string{"hf comment add --task <task-code> --content <text>"}, Flags: authFlagHelp()},
|
||||
"comment/list": {Summary: "List comments for a task", Usage: []string{"hf comment list --task <task-code>"}, Flags: authFlagHelp()},
|
||||
"worklog/add": {Summary: "Add a work log entry", Usage: []string{"hf worklog add --task <task-code> --hours <n> [--desc <text>] [--date <yyyy-mm-dd>]"}, Flags: authFlagHelp()},
|
||||
|
||||
@@ -139,6 +139,21 @@ func CommandSurface() []Group {
|
||||
{Name: "essential", Description: "Manage proposal essentials", Permitted: has(perms, "task.create")},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "calendar",
|
||||
Description: "Manage calendar slots and plans",
|
||||
SubCommands: []Command{
|
||||
{Name: "schedule", Description: "Create a one-off slot", Permitted: has(perms, "task.create")},
|
||||
{Name: "show", Description: "Show slots for a day", Permitted: has(perms, "task.read")},
|
||||
{Name: "edit", Description: "Edit a slot", Permitted: has(perms, "task.write")},
|
||||
{Name: "cancel", Description: "Cancel a slot", Permitted: has(perms, "task.write")},
|
||||
{Name: "date-list", Description: "List dates with materialized slots", Permitted: has(perms, "task.read")},
|
||||
{Name: "plan-schedule", Description: "Create a recurring plan", Permitted: has(perms, "task.create")},
|
||||
{Name: "plan-list", Description: "List plans", Permitted: has(perms, "task.read")},
|
||||
{Name: "plan-edit", Description: "Edit a plan", Permitted: has(perms, "task.write")},
|
||||
{Name: "plan-cancel", Description: "Cancel a plan", Permitted: has(perms, "task.write")},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "comment",
|
||||
Description: "Manage task comments",
|
||||
|
||||
Reference in New Issue
Block a user