1
0

task.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  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. c := client.New(cfg.ServerURL)
  341. c.SetToken(cfg.Token)
  342. mutation := `mutation CreateTask($input: NewTask!) { createTask(input: $input) { id title priority createdBy { id email } assignee { id email } status { id code label } dueDate createdAt updatedAt } }`
  343. input := map[string]interface{}{
  344. "title": title,
  345. "content": content,
  346. "createdById": createdBy,
  347. "priority": priority,
  348. }
  349. if assignee != "" {
  350. input["assigneeId"] = assignee
  351. }
  352. if status != "" {
  353. input["statusId"] = status
  354. }
  355. if dueDate != "" {
  356. input["dueDate"] = dueDate
  357. }
  358. resp, err := c.Mutation(mutation, map[string]interface{}{"input": input})
  359. if err != nil {
  360. return err
  361. }
  362. var result struct {
  363. CreateTask *Task `json:"createTask"`
  364. }
  365. if err := json.Unmarshal(resp.Data, &result); err != nil {
  366. return err
  367. }
  368. if result.CreateTask == nil {
  369. return fmt.Errorf("failed to create task")
  370. }
  371. fmt.Printf("Task created successfully!\n")
  372. fmt.Printf("ID: %s\n", result.CreateTask.ID)
  373. fmt.Printf("Title: %s\n", result.CreateTask.Title)
  374. return nil
  375. }
  376. func taskUpdate(ctx context.Context, cmd *cli.Command) error {
  377. cfg, err := config.Load()
  378. if err != nil {
  379. return err
  380. }
  381. if err := RequireAuth(cfg); err != nil {
  382. return err
  383. }
  384. id := cmd.String("id")
  385. title := cmd.String("title")
  386. content := cmd.String("content")
  387. assignee := cmd.String("assignee")
  388. status := cmd.String("status")
  389. dueDate := cmd.String("due-date")
  390. priority := cmd.String("priority")
  391. if title == "" && content == "" && assignee == "" && status == "" && dueDate == "" && priority == "" {
  392. fmt.Println("No updates provided. Use flags to specify what to update.")
  393. return nil
  394. }
  395. c := client.New(cfg.ServerURL)
  396. c.SetToken(cfg.Token)
  397. input := make(map[string]interface{})
  398. if title != "" {
  399. input["title"] = title
  400. }
  401. if content != "" {
  402. input["content"] = content
  403. }
  404. if assignee != "" {
  405. input["assigneeId"] = assignee
  406. }
  407. if status != "" {
  408. input["statusId"] = status
  409. }
  410. if dueDate != "" {
  411. input["dueDate"] = dueDate
  412. }
  413. if priority != "" {
  414. input["priority"] = priority
  415. }
  416. mutation := `mutation UpdateTask($id: ID!, $input: UpdateTaskInput!) { updateTask(id: $id, input: $input) { id title priority status { id code label } createdAt updatedAt } }`
  417. resp, err := c.Mutation(mutation, map[string]interface{}{"id": id, "input": input})
  418. if err != nil {
  419. return err
  420. }
  421. var result struct {
  422. UpdateTask *Task `json:"updateTask"`
  423. }
  424. if err := json.Unmarshal(resp.Data, &result); err != nil {
  425. return err
  426. }
  427. if result.UpdateTask == nil {
  428. return fmt.Errorf("task not found")
  429. }
  430. fmt.Printf("Task updated successfully!\n")
  431. fmt.Printf("ID: %s\n", result.UpdateTask.ID)
  432. fmt.Printf("Title: %s\n", result.UpdateTask.Title)
  433. return nil
  434. }
  435. func taskDelete(ctx context.Context, cmd *cli.Command) error {
  436. cfg, err := config.Load()
  437. if err != nil {
  438. return err
  439. }
  440. if err := RequireAuth(cfg); err != nil {
  441. return err
  442. }
  443. id := cmd.String("id")
  444. skipConfirm := cmd.Bool("yes")
  445. if !skipConfirm {
  446. confirm := false
  447. prompt := &survey.Confirm{
  448. Message: fmt.Sprintf("Are you sure you want to delete task %s?", id),
  449. Default: false,
  450. }
  451. if err := survey.AskOne(prompt, &confirm); err != nil {
  452. return err
  453. }
  454. if !confirm {
  455. fmt.Println("Deletion cancelled.")
  456. return nil
  457. }
  458. }
  459. c := client.New(cfg.ServerURL)
  460. c.SetToken(cfg.Token)
  461. mutation := `mutation DeleteTask($id: ID!) { deleteTask(id: $id) }`
  462. resp, err := c.Mutation(mutation, map[string]interface{}{"id": id})
  463. if err != nil {
  464. return err
  465. }
  466. var result struct {
  467. DeleteTask bool `json:"deleteTask"`
  468. }
  469. if err := json.Unmarshal(resp.Data, &result); err != nil {
  470. return err
  471. }
  472. if result.DeleteTask {
  473. fmt.Printf("Task %s deleted successfully.\n", id)
  474. } else {
  475. fmt.Printf("Failed to delete task %s.\n", id)
  476. }
  477. return nil
  478. }
  479. func taskWatch(ctx context.Context, cmd *cli.Command) error {
  480. cfg, err := config.Load()
  481. if err != nil {
  482. return err
  483. }
  484. if err := RequireAuth(cfg); err != nil {
  485. return err
  486. }
  487. eventType := cmd.String("event")
  488. wsClient := client.NewWebSocketClient(cfg.ServerURL, cfg.Token)
  489. if err := wsClient.Connect(); err != nil {
  490. return fmt.Errorf("failed to connect: %w", err)
  491. }
  492. defer wsClient.Close()
  493. fmt.Printf("Watching for task events (type: %s)...\n", eventType)
  494. fmt.Println("Press Ctrl+C to stop.")
  495. var subscription string
  496. switch eventType {
  497. case "created":
  498. subscription = "subscription { taskCreated { id title priority status { id code label } createdAt } }"
  499. case "updated":
  500. subscription = "subscription { taskUpdated { id title priority status { id code label } updatedAt } }"
  501. case "deleted":
  502. subscription = "subscription { taskDeleted { id title } }"
  503. default:
  504. subscription = "subscription { taskCreated { id title priority status { id code label } createdAt } taskUpdated { id title priority status { id code label } updatedAt } taskDeleted { id title } }"
  505. }
  506. if err := wsClient.Subscribe("1", subscription, nil); err != nil {
  507. return fmt.Errorf("failed to subscribe: %w", err)
  508. }
  509. for {
  510. select {
  511. case msg := <-wsClient.Messages():
  512. fmt.Printf("Event: %s\n", string(msg))
  513. case err := <-wsClient.Errors():
  514. fmt.Fprintf(os.Stderr, "Error: %v\n", err)
  515. case <-wsClient.Done():
  516. return nil
  517. case <-ctx.Done():
  518. return nil
  519. }
  520. }
  521. }