task.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. package cmd
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "os"
  7. "gogs.dmsc.dev/arp/arp_cli/client"
  8. "gogs.dmsc.dev/arp/arp_cli/config"
  9. "github.com/AlecAivazis/survey/v2"
  10. "github.com/olekukonko/tablewriter"
  11. "github.com/urfave/cli/v3"
  12. )
  13. // TaskCommand returns the task command
  14. func TaskCommand() *cli.Command {
  15. return &cli.Command{
  16. Name: "task",
  17. Usage: "Manage tasks",
  18. Description: `Manage ARP tasks. Tasks are work items that can be assigned to users.
  19. Tasks have statuses, priorities, and due dates. Use this command to
  20. create, list, update, and delete tasks. You can also watch for real-time task updates.`,
  21. Commands: []*cli.Command{
  22. {
  23. Name: "list",
  24. Aliases: []string{"ls"},
  25. Usage: "List all tasks",
  26. Flags: []cli.Flag{
  27. &cli.BoolFlag{
  28. Name: "json",
  29. Aliases: []string{"j"},
  30. Usage: "Output as JSON",
  31. },
  32. },
  33. Action: taskList,
  34. },
  35. {
  36. Name: "get",
  37. Usage: "Get a task by ID",
  38. Flags: []cli.Flag{
  39. &cli.StringFlag{
  40. Name: "id",
  41. Aliases: []string{"i"},
  42. Usage: "Task ID",
  43. Required: true,
  44. },
  45. &cli.BoolFlag{
  46. Name: "json",
  47. Aliases: []string{"j"},
  48. Usage: "Output as JSON",
  49. },
  50. },
  51. Action: taskGet,
  52. },
  53. {
  54. Name: "create",
  55. Usage: "Create a new task",
  56. Action: taskCreate,
  57. Flags: []cli.Flag{
  58. &cli.StringFlag{
  59. Name: "title",
  60. Aliases: []string{"t"},
  61. Usage: "Task title",
  62. },
  63. &cli.StringFlag{
  64. Name: "content",
  65. Aliases: []string{"c"},
  66. Usage: "Task content",
  67. },
  68. &cli.StringFlag{
  69. Name: "created-by",
  70. Aliases: []string{"b"},
  71. Usage: "Creator user ID",
  72. },
  73. &cli.StringFlag{
  74. Name: "assignee",
  75. Aliases: []string{"a"},
  76. Usage: "Assignee user ID",
  77. },
  78. &cli.StringFlag{
  79. Name: "status",
  80. Aliases: []string{"s"},
  81. Usage: "Status ID",
  82. },
  83. &cli.StringFlag{
  84. Name: "due-date",
  85. Aliases: []string{"d"},
  86. Usage: "Due date",
  87. },
  88. &cli.StringFlag{
  89. Name: "priority",
  90. Aliases: []string{"p"},
  91. Usage: "Priority (low, medium, high)",
  92. },
  93. },
  94. },
  95. {
  96. Name: "update",
  97. Usage: "Update a task",
  98. Action: taskUpdate,
  99. Flags: []cli.Flag{
  100. &cli.StringFlag{
  101. Name: "id",
  102. Aliases: []string{"i"},
  103. Usage: "Task ID",
  104. Required: true,
  105. },
  106. &cli.StringFlag{
  107. Name: "title",
  108. Aliases: []string{"t"},
  109. Usage: "Task title",
  110. },
  111. &cli.StringFlag{
  112. Name: "content",
  113. Aliases: []string{"c"},
  114. Usage: "Task content",
  115. },
  116. &cli.StringFlag{
  117. Name: "assignee",
  118. Aliases: []string{"a"},
  119. Usage: "Assignee user ID",
  120. },
  121. &cli.StringFlag{
  122. Name: "status",
  123. Aliases: []string{"s"},
  124. Usage: "Status ID",
  125. },
  126. &cli.StringFlag{
  127. Name: "due-date",
  128. Aliases: []string{"d"},
  129. Usage: "Due date",
  130. },
  131. &cli.StringFlag{
  132. Name: "priority",
  133. Aliases: []string{"p"},
  134. Usage: "Priority",
  135. },
  136. },
  137. },
  138. {
  139. Name: "delete",
  140. Usage: "Delete a task",
  141. Action: taskDelete,
  142. Flags: []cli.Flag{
  143. &cli.StringFlag{
  144. Name: "id",
  145. Aliases: []string{"i"},
  146. Usage: "Task ID",
  147. Required: true,
  148. },
  149. &cli.BoolFlag{
  150. Name: "yes",
  151. Aliases: []string{"y"},
  152. Usage: "Skip confirmation",
  153. },
  154. },
  155. },
  156. {
  157. Name: "watch",
  158. Usage: "Watch for real-time task updates",
  159. Action: taskWatch,
  160. Flags: []cli.Flag{
  161. &cli.StringFlag{
  162. Name: "event",
  163. Aliases: []string{"e"},
  164. Usage: "Event type (created, updated, deleted, all)",
  165. Value: "all",
  166. },
  167. },
  168. },
  169. },
  170. }
  171. }
  172. type Task struct {
  173. ID string `json:"id"`
  174. Title string `json:"title"`
  175. Content string `json:"content"`
  176. CreatedByID string `json:"createdById"`
  177. CreatedBy *User `json:"createdBy"`
  178. UpdatedByID string `json:"updatedById"`
  179. UpdatedBy *User `json:"updatedBy"`
  180. AssigneeID *string `json:"assigneeId"`
  181. Assignee *User `json:"assignee"`
  182. StatusID *string `json:"statusId"`
  183. Status *TaskStatus `json:"status"`
  184. DueDate *string `json:"dueDate"`
  185. Priority string `json:"priority"`
  186. CreatedAt string `json:"createdAt"`
  187. UpdatedAt string `json:"updatedAt"`
  188. }
  189. type TaskStatus struct {
  190. ID string `json:"id"`
  191. Code string `json:"code"`
  192. Label string `json:"label"`
  193. CreatedAt string `json:"createdAt"`
  194. UpdatedAt string `json:"updatedAt"`
  195. }
  196. func taskList(ctx context.Context, cmd *cli.Command) error {
  197. cfg, err := config.Load()
  198. if err != nil {
  199. return err
  200. }
  201. if err := RequireAuth(cfg); err != nil {
  202. return err
  203. }
  204. c := client.New(cfg.ServerURL)
  205. c.SetToken(cfg.Token)
  206. query := "query Tasks { tasks { id title priority createdBy { id email } assignee { id email } status { id code label } dueDate createdAt updatedAt } }"
  207. resp, err := c.Query(query, nil)
  208. if err != nil {
  209. return err
  210. }
  211. var result struct {
  212. Tasks []Task `json:"tasks"`
  213. }
  214. if err := json.Unmarshal(resp.Data, &result); err != nil {
  215. return err
  216. }
  217. if cmd.Bool("json") {
  218. enc := json.NewEncoder(os.Stdout)
  219. enc.SetIndent("", " ")
  220. return enc.Encode(result.Tasks)
  221. }
  222. if len(result.Tasks) == 0 {
  223. fmt.Println("No tasks found.")
  224. return nil
  225. }
  226. table := tablewriter.NewWriter(os.Stdout)
  227. table.Header([]string{"ID", "Title", "Priority", "Status", "Assignee", "Due Date"})
  228. for _, t := range result.Tasks {
  229. status := ""
  230. if t.Status != nil {
  231. status = t.Status.Label
  232. }
  233. assignee := ""
  234. if t.Assignee != nil {
  235. assignee = t.Assignee.Email
  236. }
  237. dueDate := ""
  238. if t.DueDate != nil {
  239. dueDate = *t.DueDate
  240. }
  241. table.Append([]string{t.ID, t.Title, t.Priority, status, assignee, dueDate})
  242. }
  243. table.Render()
  244. return nil
  245. }
  246. func taskGet(ctx context.Context, cmd *cli.Command) error {
  247. cfg, err := config.Load()
  248. if err != nil {
  249. return err
  250. }
  251. if err := RequireAuth(cfg); err != nil {
  252. return err
  253. }
  254. c := client.New(cfg.ServerURL)
  255. c.SetToken(cfg.Token)
  256. id := cmd.String("id")
  257. 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 } }"
  258. resp, err := c.Query(query, map[string]interface{}{"id": id})
  259. if err != nil {
  260. return err
  261. }
  262. var result struct {
  263. Task *Task `json:"task"`
  264. }
  265. if err := json.Unmarshal(resp.Data, &result); err != nil {
  266. return err
  267. }
  268. if result.Task == nil {
  269. return fmt.Errorf("task not found")
  270. }
  271. if cmd.Bool("json") {
  272. enc := json.NewEncoder(os.Stdout)
  273. enc.SetIndent("", " ")
  274. return enc.Encode(result.Task)
  275. }
  276. t := result.Task
  277. fmt.Printf("ID: %s\n", t.ID)
  278. fmt.Printf("Title: %s\n", t.Title)
  279. fmt.Printf("Content: %s\n", t.Content)
  280. if t.CreatedBy != nil {
  281. fmt.Printf("Created By: %s\n", t.CreatedBy.Email)
  282. }
  283. if t.Assignee != nil {
  284. fmt.Printf("Assignee: %s\n", t.Assignee.Email)
  285. }
  286. if t.Status != nil {
  287. fmt.Printf("Status: %s (%s)\n", t.Status.Label, t.Status.Code)
  288. }
  289. if t.DueDate != nil {
  290. fmt.Printf("Due Date: %s\n", *t.DueDate)
  291. }
  292. fmt.Printf("Priority: %s\n", t.Priority)
  293. fmt.Printf("Created At: %s\n", t.CreatedAt)
  294. fmt.Printf("Updated At: %s\n", t.UpdatedAt)
  295. return nil
  296. }
  297. func taskCreate(ctx context.Context, cmd *cli.Command) error {
  298. cfg, err := config.Load()
  299. if err != nil {
  300. return err
  301. }
  302. if err := RequireAuth(cfg); err != nil {
  303. return err
  304. }
  305. title := cmd.String("title")
  306. content := cmd.String("content")
  307. createdBy := cmd.String("created-by")
  308. assignee := cmd.String("assignee")
  309. status := cmd.String("status")
  310. dueDate := cmd.String("due-date")
  311. priority := cmd.String("priority")
  312. if title == "" {
  313. prompt := &survey.Input{Message: "Title:"}
  314. if err := survey.AskOne(prompt, &title, survey.WithValidator(survey.Required)); err != nil {
  315. return err
  316. }
  317. }
  318. if content == "" {
  319. prompt := &survey.Multiline{Message: "Content:"}
  320. if err := survey.AskOne(prompt, &content, survey.WithValidator(survey.Required)); err != nil {
  321. return err
  322. }
  323. }
  324. if createdBy == "" {
  325. prompt := &survey.Input{Message: "Creator user ID:"}
  326. if err := survey.AskOne(prompt, &createdBy, survey.WithValidator(survey.Required)); err != nil {
  327. return err
  328. }
  329. }
  330. if priority == "" {
  331. prompt := &survey.Select{
  332. Message: "Priority:",
  333. Options: []string{"low", "medium", "high"},
  334. Default: "medium",
  335. }
  336. if err := survey.AskOne(prompt, &priority); err != nil {
  337. return err
  338. }
  339. }
  340. // Prompt for assignee (optional)
  341. if assignee == "" {
  342. prompt := &survey.Input{Message: "Assignee user ID (optional, press Enter to skip):"}
  343. if err := survey.AskOne(prompt, &assignee); err != nil {
  344. return err
  345. }
  346. }
  347. // Prompt for status (optional)
  348. if status == "" {
  349. prompt := &survey.Input{Message: "Status ID (optional, press Enter to skip):"}
  350. if err := survey.AskOne(prompt, &status); err != nil {
  351. return err
  352. }
  353. }
  354. // Prompt for due date (optional)
  355. if dueDate == "" {
  356. prompt := &survey.Input{Message: "Due date (optional, press Enter to skip):"}
  357. if err := survey.AskOne(prompt, &dueDate); err != nil {
  358. return err
  359. }
  360. }
  361. c := client.New(cfg.ServerURL)
  362. c.SetToken(cfg.Token)
  363. mutation := `mutation CreateTask($input: NewTask!) { createTask(input: $input) { id title priority createdBy { id email } assignee { id email } status { id code label } dueDate createdAt updatedAt } }`
  364. input := map[string]interface{}{
  365. "title": title,
  366. "content": content,
  367. "createdById": createdBy,
  368. "priority": priority,
  369. }
  370. if assignee != "" {
  371. input["assigneeId"] = assignee
  372. }
  373. if status != "" {
  374. input["statusId"] = status
  375. }
  376. if dueDate != "" {
  377. input["dueDate"] = dueDate
  378. }
  379. resp, err := c.Mutation(mutation, map[string]interface{}{"input": input})
  380. if err != nil {
  381. return err
  382. }
  383. var result struct {
  384. CreateTask *Task `json:"createTask"`
  385. }
  386. if err := json.Unmarshal(resp.Data, &result); err != nil {
  387. return err
  388. }
  389. if result.CreateTask == nil {
  390. return fmt.Errorf("failed to create task")
  391. }
  392. fmt.Printf("Task created successfully!\n")
  393. fmt.Printf("ID: %s\n", result.CreateTask.ID)
  394. fmt.Printf("Title: %s\n", result.CreateTask.Title)
  395. return nil
  396. }
  397. func taskUpdate(ctx context.Context, cmd *cli.Command) error {
  398. cfg, err := config.Load()
  399. if err != nil {
  400. return err
  401. }
  402. if err := RequireAuth(cfg); err != nil {
  403. return err
  404. }
  405. id := cmd.String("id")
  406. title := cmd.String("title")
  407. content := cmd.String("content")
  408. assignee := cmd.String("assignee")
  409. status := cmd.String("status")
  410. dueDate := cmd.String("due-date")
  411. priority := cmd.String("priority")
  412. if title == "" && content == "" && assignee == "" && status == "" && dueDate == "" && priority == "" {
  413. fmt.Println("No updates provided. Use flags to specify what to update.")
  414. return nil
  415. }
  416. c := client.New(cfg.ServerURL)
  417. c.SetToken(cfg.Token)
  418. input := make(map[string]interface{})
  419. if title != "" {
  420. input["title"] = title
  421. }
  422. if content != "" {
  423. input["content"] = content
  424. }
  425. if assignee != "" {
  426. input["assigneeId"] = assignee
  427. }
  428. if status != "" {
  429. input["statusId"] = status
  430. }
  431. if dueDate != "" {
  432. input["dueDate"] = dueDate
  433. }
  434. if priority != "" {
  435. input["priority"] = priority
  436. }
  437. mutation := `mutation UpdateTask($id: ID!, $input: UpdateTaskInput!) { updateTask(id: $id, input: $input) { id title priority status { id code label } createdAt updatedAt } }`
  438. resp, err := c.Mutation(mutation, map[string]interface{}{"id": id, "input": input})
  439. if err != nil {
  440. return err
  441. }
  442. var result struct {
  443. UpdateTask *Task `json:"updateTask"`
  444. }
  445. if err := json.Unmarshal(resp.Data, &result); err != nil {
  446. return err
  447. }
  448. if result.UpdateTask == nil {
  449. return fmt.Errorf("task not found")
  450. }
  451. fmt.Printf("Task updated successfully!\n")
  452. fmt.Printf("ID: %s\n", result.UpdateTask.ID)
  453. fmt.Printf("Title: %s\n", result.UpdateTask.Title)
  454. return nil
  455. }
  456. func taskDelete(ctx context.Context, cmd *cli.Command) error {
  457. cfg, err := config.Load()
  458. if err != nil {
  459. return err
  460. }
  461. if err := RequireAuth(cfg); err != nil {
  462. return err
  463. }
  464. id := cmd.String("id")
  465. skipConfirm := cmd.Bool("yes")
  466. if !skipConfirm {
  467. confirm := false
  468. prompt := &survey.Confirm{
  469. Message: fmt.Sprintf("Are you sure you want to delete task %s?", id),
  470. Default: false,
  471. }
  472. if err := survey.AskOne(prompt, &confirm); err != nil {
  473. return err
  474. }
  475. if !confirm {
  476. fmt.Println("Deletion cancelled.")
  477. return nil
  478. }
  479. }
  480. c := client.New(cfg.ServerURL)
  481. c.SetToken(cfg.Token)
  482. mutation := `mutation DeleteTask($id: ID!) { deleteTask(id: $id) }`
  483. resp, err := c.Mutation(mutation, map[string]interface{}{"id": id})
  484. if err != nil {
  485. return err
  486. }
  487. var result struct {
  488. DeleteTask bool `json:"deleteTask"`
  489. }
  490. if err := json.Unmarshal(resp.Data, &result); err != nil {
  491. return err
  492. }
  493. if result.DeleteTask {
  494. fmt.Printf("Task %s deleted successfully.\n", id)
  495. } else {
  496. fmt.Printf("Failed to delete task %s.\n", id)
  497. }
  498. return nil
  499. }
  500. func taskWatch(ctx context.Context, cmd *cli.Command) error {
  501. cfg, err := config.Load()
  502. if err != nil {
  503. return err
  504. }
  505. if err := RequireAuth(cfg); err != nil {
  506. return err
  507. }
  508. eventType := cmd.String("event")
  509. wsClient := client.NewWebSocketClient(cfg.ServerURL, cfg.Token)
  510. if err := wsClient.Connect(); err != nil {
  511. return fmt.Errorf("failed to connect: %w", err)
  512. }
  513. defer wsClient.Close()
  514. fmt.Printf("Watching for task events (type: %s)...\n", eventType)
  515. fmt.Println("Press Ctrl+C to stop.")
  516. // GraphQL subscriptions only allow one top-level field per subscription
  517. // When watching "all" events, we need to create separate subscriptions
  518. switch eventType {
  519. case "created":
  520. subscription := "subscription { taskCreated { id title priority status { id code label } createdAt } }"
  521. if err := wsClient.Subscribe("1", subscription, nil); err != nil {
  522. return fmt.Errorf("failed to subscribe: %w", err)
  523. }
  524. case "updated":
  525. subscription := "subscription { taskUpdated { id title priority status { id code label } updatedAt } }"
  526. if err := wsClient.Subscribe("1", subscription, nil); err != nil {
  527. return fmt.Errorf("failed to subscribe: %w", err)
  528. }
  529. case "deleted":
  530. subscription := "subscription { taskDeleted { id title } }"
  531. if err := wsClient.Subscribe("1", subscription, nil); err != nil {
  532. return fmt.Errorf("failed to subscribe: %w", err)
  533. }
  534. default:
  535. // Subscribe to all three event types with separate subscriptions
  536. subscriptions := []struct {
  537. id string
  538. query string
  539. }{
  540. {"1", "subscription { taskCreated { id title priority status { id code label } createdAt } }"},
  541. {"2", "subscription { taskUpdated { id title priority status { id code label } updatedAt } }"},
  542. {"3", "subscription { taskDeleted { id title } }"},
  543. }
  544. for _, sub := range subscriptions {
  545. if err := wsClient.Subscribe(sub.id, sub.query, nil); err != nil {
  546. return fmt.Errorf("failed to subscribe to %s: %w", sub.id, err)
  547. }
  548. }
  549. }
  550. for {
  551. select {
  552. case msg := <-wsClient.Messages():
  553. fmt.Printf("Event: %s\n", string(msg))
  554. case err := <-wsClient.Errors():
  555. fmt.Fprintf(os.Stderr, "Error: %v\n", err)
  556. case <-wsClient.Done():
  557. return nil
  558. case <-ctx.Done():
  559. return nil
  560. }
  561. }
  562. }