workflow.go 29 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121
  1. package cmd
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "os"
  7. "strings"
  8. "github.com/AlecAivazis/survey/v2"
  9. "github.com/olekukonko/tablewriter"
  10. "github.com/urfave/cli/v3"
  11. )
  12. // WorkflowCommand returns the workflow command
  13. func WorkflowCommand() *cli.Command {
  14. return &cli.Command{
  15. Name: "workflow",
  16. Usage: "Manage workflows",
  17. Description: `Manage ARP workflows. Workflows are automated processes defined by templates.
  18. Workflow templates define the structure of automated workflows as DAGs (Directed Acyclic Graphs).
  19. Workflow instances are running executions of templates.
  20. Workflow nodes represent individual steps in a running workflow.`,
  21. Commands: []*cli.Command{
  22. // WorkflowTemplate commands
  23. {
  24. Name: "template",
  25. Usage: "Manage workflow templates",
  26. Commands: []*cli.Command{
  27. {
  28. Name: "list",
  29. Aliases: []string{"ls"},
  30. Usage: "List all workflow templates",
  31. Flags: []cli.Flag{
  32. &cli.BoolFlag{
  33. Name: "json",
  34. Aliases: []string{"j"},
  35. Usage: "Output as JSON",
  36. },
  37. },
  38. Action: workflowTemplateList,
  39. },
  40. {
  41. Name: "get",
  42. Usage: "Get a workflow template by ID",
  43. Flags: []cli.Flag{
  44. &cli.StringFlag{
  45. Name: "id",
  46. Aliases: []string{"i"},
  47. Usage: "Template ID",
  48. Required: true,
  49. },
  50. &cli.BoolFlag{
  51. Name: "json",
  52. Aliases: []string{"j"},
  53. Usage: "Output as JSON",
  54. },
  55. },
  56. Action: workflowTemplateGet,
  57. },
  58. {
  59. Name: "create",
  60. Usage: "Create a new workflow template",
  61. Action: workflowTemplateCreate,
  62. Flags: []cli.Flag{
  63. &cli.StringFlag{
  64. Name: "file",
  65. Aliases: []string{"f"},
  66. Usage: "Path to JSON file containing template data",
  67. },
  68. &cli.StringFlag{
  69. Name: "name",
  70. Aliases: []string{"n"},
  71. Usage: "Template name",
  72. },
  73. &cli.StringFlag{
  74. Name: "description",
  75. Aliases: []string{"d"},
  76. Usage: "Template description",
  77. },
  78. &cli.StringFlag{
  79. Name: "definition",
  80. Aliases: []string{"D"},
  81. Usage: "Workflow definition (JSON string or @filename)",
  82. },
  83. &cli.BoolFlag{
  84. Name: "active",
  85. Usage: "Set template as active",
  86. Value: true,
  87. },
  88. },
  89. },
  90. {
  91. Name: "update",
  92. Usage: "Update a workflow template",
  93. Action: workflowTemplateUpdate,
  94. Flags: []cli.Flag{
  95. &cli.StringFlag{
  96. Name: "id",
  97. Aliases: []string{"i"},
  98. Usage: "Template ID",
  99. Required: true,
  100. },
  101. &cli.StringFlag{
  102. Name: "name",
  103. Aliases: []string{"n"},
  104. Usage: "Template name",
  105. },
  106. &cli.StringFlag{
  107. Name: "description",
  108. Aliases: []string{"d"},
  109. Usage: "Template description",
  110. },
  111. &cli.StringFlag{
  112. Name: "definition",
  113. Aliases: []string{"f"},
  114. Usage: "Workflow definition (JSON string or @filename)",
  115. },
  116. &cli.BoolFlag{
  117. Name: "active",
  118. Usage: "Set template as active",
  119. },
  120. },
  121. },
  122. {
  123. Name: "delete",
  124. Usage: "Delete a workflow template",
  125. Action: workflowTemplateDelete,
  126. Flags: []cli.Flag{
  127. &cli.StringFlag{
  128. Name: "id",
  129. Aliases: []string{"i"},
  130. Usage: "Template ID",
  131. Required: true,
  132. },
  133. &cli.BoolFlag{
  134. Name: "yes",
  135. Aliases: []string{"y"},
  136. Usage: "Skip confirmation",
  137. },
  138. },
  139. },
  140. {
  141. Name: "example",
  142. Usage: "Generate an example template file",
  143. Action: workflowTemplateExample,
  144. Flags: []cli.Flag{
  145. &cli.StringFlag{
  146. Name: "output",
  147. Aliases: []string{"o"},
  148. Usage: "Output file path",
  149. Value: "workflow_template.json",
  150. },
  151. },
  152. },
  153. },
  154. },
  155. // WorkflowInstance commands
  156. {
  157. Name: "instance",
  158. Usage: "Manage workflow instances",
  159. Commands: []*cli.Command{
  160. {
  161. Name: "list",
  162. Aliases: []string{"ls"},
  163. Usage: "List all workflow instances",
  164. Flags: []cli.Flag{
  165. &cli.BoolFlag{
  166. Name: "json",
  167. Aliases: []string{"j"},
  168. Usage: "Output as JSON",
  169. },
  170. },
  171. Action: workflowInstanceList,
  172. },
  173. {
  174. Name: "get",
  175. Usage: "Get a workflow instance by ID",
  176. Flags: []cli.Flag{
  177. &cli.StringFlag{
  178. Name: "id",
  179. Aliases: []string{"i"},
  180. Usage: "Instance ID",
  181. Required: true,
  182. },
  183. &cli.BoolFlag{
  184. Name: "json",
  185. Aliases: []string{"j"},
  186. Usage: "Output as JSON",
  187. },
  188. },
  189. Action: workflowInstanceGet,
  190. },
  191. {
  192. Name: "start",
  193. Usage: "Start a new workflow instance from a template",
  194. Action: workflowInstanceStart,
  195. Flags: []cli.Flag{
  196. &cli.StringFlag{
  197. Name: "template",
  198. Aliases: []string{"t"},
  199. Usage: "Template ID",
  200. Required: true,
  201. },
  202. &cli.StringFlag{
  203. Name: "service",
  204. Aliases: []string{"s"},
  205. Usage: "Service ID to associate with the workflow",
  206. },
  207. &cli.StringFlag{
  208. Name: "context",
  209. Aliases: []string{"c"},
  210. Usage: "Initial workflow context (JSON string)",
  211. },
  212. },
  213. },
  214. {
  215. Name: "cancel",
  216. Usage: "Cancel a running workflow instance",
  217. Action: workflowInstanceCancel,
  218. Flags: []cli.Flag{
  219. &cli.StringFlag{
  220. Name: "id",
  221. Aliases: []string{"i"},
  222. Usage: "Instance ID",
  223. Required: true,
  224. },
  225. },
  226. },
  227. },
  228. },
  229. // WorkflowNode commands
  230. {
  231. Name: "node",
  232. Usage: "Manage workflow nodes",
  233. Commands: []*cli.Command{
  234. {
  235. Name: "list",
  236. Aliases: []string{"ls"},
  237. Usage: "List all nodes for a workflow instance",
  238. Flags: []cli.Flag{
  239. &cli.StringFlag{
  240. Name: "instance",
  241. Aliases: []string{"i"},
  242. Usage: "Instance ID",
  243. Required: true,
  244. },
  245. &cli.BoolFlag{
  246. Name: "json",
  247. Aliases: []string{"j"},
  248. Usage: "Output as JSON",
  249. },
  250. },
  251. Action: workflowNodeList,
  252. },
  253. {
  254. Name: "get",
  255. Usage: "Get a workflow node by ID",
  256. Flags: []cli.Flag{
  257. &cli.StringFlag{
  258. Name: "id",
  259. Aliases: []string{"i"},
  260. Usage: "Node ID",
  261. Required: true,
  262. },
  263. &cli.BoolFlag{
  264. Name: "json",
  265. Aliases: []string{"j"},
  266. Usage: "Output as JSON",
  267. },
  268. },
  269. Action: workflowNodeGet,
  270. },
  271. {
  272. Name: "retry",
  273. Usage: "Retry a failed workflow node",
  274. Action: workflowNodeRetry,
  275. Flags: []cli.Flag{
  276. &cli.StringFlag{
  277. Name: "id",
  278. Aliases: []string{"i"},
  279. Usage: "Node ID",
  280. Required: true,
  281. },
  282. },
  283. },
  284. },
  285. },
  286. },
  287. }
  288. }
  289. // WorkflowTemplateFile represents a workflow template stored in a JSON file
  290. type WorkflowTemplateFile struct {
  291. Name string `json:"name"`
  292. Description string `json:"description"`
  293. Definition interface{} `json:"definition"`
  294. IsActive bool `json:"isActive"`
  295. }
  296. // WorkflowTemplate represents a workflow template
  297. type WorkflowTemplate struct {
  298. ID string `json:"id"`
  299. Name string `json:"name"`
  300. Description string `json:"description"`
  301. Definition string `json:"definition"`
  302. IsActive bool `json:"isActive"`
  303. CreatedBy *User `json:"createdBy"`
  304. CreatedAt string `json:"createdAt"`
  305. UpdatedAt string `json:"updatedAt"`
  306. }
  307. // WorkflowInstance represents a running workflow instance
  308. type WorkflowInstance struct {
  309. ID string `json:"id"`
  310. Template *WorkflowTemplate `json:"template"`
  311. Status string `json:"status"`
  312. Context string `json:"context"`
  313. Service *Service `json:"service"`
  314. CreatedAt string `json:"createdAt"`
  315. UpdatedAt string `json:"updatedAt"`
  316. CompletedAt *string `json:"completedAt"`
  317. }
  318. // WorkflowNode represents a node in a workflow instance
  319. type WorkflowNode struct {
  320. ID string `json:"id"`
  321. NodeKey string `json:"nodeKey"`
  322. NodeType string `json:"nodeType"`
  323. Status string `json:"status"`
  324. Task *Task `json:"task"`
  325. InputData string `json:"inputData"`
  326. OutputData string `json:"outputData"`
  327. RetryCount int `json:"retryCount"`
  328. CreatedAt string `json:"createdAt"`
  329. UpdatedAt string `json:"updatedAt"`
  330. StartedAt *string `json:"startedAt"`
  331. CompletedAt *string `json:"completedAt"`
  332. }
  333. // WorkflowTemplate CRUD operations
  334. func workflowTemplateList(ctx context.Context, cmd *cli.Command) error {
  335. c, cfg, err := GetClient(ctx, cmd)
  336. if err != nil {
  337. return err
  338. }
  339. if err := RequireAuth(cfg); err != nil {
  340. return err
  341. }
  342. query := `query WorkflowTemplates { workflowTemplates { id name description definition isActive createdBy { id email } createdAt updatedAt } }`
  343. resp, err := c.Query(query, nil)
  344. if err != nil {
  345. return err
  346. }
  347. var result struct {
  348. WorkflowTemplates []WorkflowTemplate `json:"workflowTemplates"`
  349. }
  350. if err := json.Unmarshal(resp.Data, &result); err != nil {
  351. return err
  352. }
  353. if cmd.Bool("json") {
  354. enc := json.NewEncoder(os.Stdout)
  355. enc.SetIndent("", " ")
  356. return enc.Encode(result.WorkflowTemplates)
  357. }
  358. if len(result.WorkflowTemplates) == 0 {
  359. fmt.Println("No workflow templates found.")
  360. return nil
  361. }
  362. table := tablewriter.NewWriter(os.Stdout)
  363. table.Header([]string{"ID", "Name", "Description", "Active", "Created By", "Created At"})
  364. for _, t := range result.WorkflowTemplates {
  365. desc := t.Description
  366. if len(desc) > 30 {
  367. desc = desc[:27] + "..."
  368. }
  369. createdBy := ""
  370. if t.CreatedBy != nil {
  371. createdBy = t.CreatedBy.Email
  372. }
  373. active := "yes"
  374. if !t.IsActive {
  375. active = "no"
  376. }
  377. table.Append([]string{t.ID, t.Name, desc, active, createdBy, t.CreatedAt})
  378. }
  379. table.Render()
  380. return nil
  381. }
  382. func workflowTemplateGet(ctx context.Context, cmd *cli.Command) error {
  383. c, cfg, err := GetClient(ctx, cmd)
  384. if err != nil {
  385. return err
  386. }
  387. if err := RequireAuth(cfg); err != nil {
  388. return err
  389. }
  390. id := cmd.String("id")
  391. query := `query WorkflowTemplate($id: ID!) { workflowTemplate(id: $id) { id name description definition isActive createdBy { id email } createdAt updatedAt } }`
  392. resp, err := c.Query(query, map[string]interface{}{"id": id})
  393. if err != nil {
  394. return err
  395. }
  396. var result struct {
  397. WorkflowTemplate *WorkflowTemplate `json:"workflowTemplate"`
  398. }
  399. if err := json.Unmarshal(resp.Data, &result); err != nil {
  400. return err
  401. }
  402. if result.WorkflowTemplate == nil {
  403. return fmt.Errorf("workflow template not found")
  404. }
  405. if cmd.Bool("json") {
  406. enc := json.NewEncoder(os.Stdout)
  407. enc.SetIndent("", " ")
  408. return enc.Encode(result.WorkflowTemplate)
  409. }
  410. t := result.WorkflowTemplate
  411. fmt.Printf("ID: %s\n", t.ID)
  412. fmt.Printf("Name: %s\n", t.Name)
  413. fmt.Printf("Description: %s\n", t.Description)
  414. fmt.Printf("Active: %v\n", t.IsActive)
  415. if t.CreatedBy != nil {
  416. fmt.Printf("Created By: %s\n", t.CreatedBy.Email)
  417. }
  418. fmt.Printf("Created At: %s\n", t.CreatedAt)
  419. fmt.Printf("Updated At: %s\n", t.UpdatedAt)
  420. fmt.Printf("\nDefinition:\n%s\n", t.Definition)
  421. return nil
  422. }
  423. func workflowTemplateCreate(ctx context.Context, cmd *cli.Command) error {
  424. c, cfg, err := GetClient(ctx, cmd)
  425. if err != nil {
  426. return err
  427. }
  428. if err := RequireAuth(cfg); err != nil {
  429. return err
  430. }
  431. var name, description, definition string
  432. var isActive bool
  433. // Check if a file was provided
  434. filePath := cmd.String("file")
  435. if filePath != "" {
  436. // Read and parse the template file
  437. data, err := os.ReadFile(filePath)
  438. if err != nil {
  439. return fmt.Errorf("failed to read file: %w", err)
  440. }
  441. var templateFile WorkflowTemplateFile
  442. if err := json.Unmarshal(data, &templateFile); err != nil {
  443. return fmt.Errorf("failed to parse template file: %w", err)
  444. }
  445. // Validate required fields
  446. if templateFile.Name == "" {
  447. return fmt.Errorf("name is required in template file")
  448. }
  449. if templateFile.Definition == nil {
  450. return fmt.Errorf("definition is required in template file")
  451. }
  452. name = templateFile.Name
  453. description = templateFile.Description
  454. isActive = templateFile.IsActive
  455. // Marshal definition to JSON string
  456. defBytes, err := json.Marshal(templateFile.Definition)
  457. if err != nil {
  458. return fmt.Errorf("failed to marshal definition: %w", err)
  459. }
  460. definition = string(defBytes)
  461. } else {
  462. // Use command-line flags
  463. name = cmd.String("name")
  464. description = cmd.String("description")
  465. definition = cmd.String("definition")
  466. isActive = cmd.Bool("active")
  467. // Interactive prompts for missing values
  468. if name == "" {
  469. prompt := &survey.Input{Message: "Template name:"}
  470. if err := survey.AskOne(prompt, &name, survey.WithValidator(survey.Required)); err != nil {
  471. return err
  472. }
  473. }
  474. if description == "" {
  475. prompt := &survey.Input{Message: "Description (optional):"}
  476. survey.AskOne(prompt, &description)
  477. }
  478. if definition == "" {
  479. prompt := &survey.Multiline{Message: "Workflow definition (JSON):"}
  480. if err := survey.AskOne(prompt, &definition, survey.WithValidator(survey.Required)); err != nil {
  481. return err
  482. }
  483. }
  484. // Handle file input for definition
  485. definition = ReadFileOrString(definition)
  486. }
  487. mutation := `mutation CreateWorkflowTemplate($input: NewWorkflowTemplate!) { createWorkflowTemplate(input: $input) { id name description definition isActive createdBy { id email } createdAt updatedAt } }`
  488. input := map[string]interface{}{
  489. "name": name,
  490. "description": description,
  491. "definition": definition,
  492. "isActive": isActive,
  493. }
  494. resp, err := c.Mutation(mutation, map[string]interface{}{"input": input})
  495. if err != nil {
  496. return err
  497. }
  498. var result struct {
  499. CreateWorkflowTemplate *WorkflowTemplate `json:"createWorkflowTemplate"`
  500. }
  501. if err := json.Unmarshal(resp.Data, &result); err != nil {
  502. return err
  503. }
  504. if result.CreateWorkflowTemplate == nil {
  505. return fmt.Errorf("failed to create workflow template")
  506. }
  507. fmt.Printf("Workflow template created successfully!\n")
  508. fmt.Printf("ID: %s\n", result.CreateWorkflowTemplate.ID)
  509. fmt.Printf("Name: %s\n", result.CreateWorkflowTemplate.Name)
  510. return nil
  511. }
  512. func workflowTemplateUpdate(ctx context.Context, cmd *cli.Command) error {
  513. c, cfg, err := GetClient(ctx, cmd)
  514. if err != nil {
  515. return err
  516. }
  517. if err := RequireAuth(cfg); err != nil {
  518. return err
  519. }
  520. id := cmd.String("id")
  521. name := cmd.String("name")
  522. description := cmd.String("description")
  523. definition := cmd.String("definition")
  524. // Check if active flag was explicitly set
  525. var isActive *bool
  526. if cmd.IsSet("active") {
  527. val := cmd.Bool("active")
  528. isActive = &val
  529. }
  530. if name == "" && description == "" && definition == "" && isActive == nil {
  531. fmt.Println("No updates provided. Use flags to specify what to update.")
  532. return nil
  533. }
  534. input := make(map[string]interface{})
  535. if name != "" {
  536. input["name"] = name
  537. }
  538. if description != "" {
  539. input["description"] = description
  540. }
  541. if definition != "" {
  542. input["definition"] = ReadFileOrString(definition)
  543. }
  544. if isActive != nil {
  545. input["isActive"] = *isActive
  546. }
  547. mutation := `mutation UpdateWorkflowTemplate($id: ID!, $input: UpdateWorkflowTemplateInput!) { updateWorkflowTemplate(id: $id, input: $input) { id name description definition isActive createdAt updatedAt } }`
  548. resp, err := c.Mutation(mutation, map[string]interface{}{"id": id, "input": input})
  549. if err != nil {
  550. return err
  551. }
  552. var result struct {
  553. UpdateWorkflowTemplate *WorkflowTemplate `json:"updateWorkflowTemplate"`
  554. }
  555. if err := json.Unmarshal(resp.Data, &result); err != nil {
  556. return err
  557. }
  558. if result.UpdateWorkflowTemplate == nil {
  559. return fmt.Errorf("workflow template not found")
  560. }
  561. fmt.Printf("Workflow template updated successfully!\n")
  562. fmt.Printf("ID: %s\n", result.UpdateWorkflowTemplate.ID)
  563. fmt.Printf("Name: %s\n", result.UpdateWorkflowTemplate.Name)
  564. return nil
  565. }
  566. func workflowTemplateDelete(ctx context.Context, cmd *cli.Command) error {
  567. c, cfg, err := GetClient(ctx, cmd)
  568. if err != nil {
  569. return err
  570. }
  571. if err := RequireAuth(cfg); err != nil {
  572. return err
  573. }
  574. id := cmd.String("id")
  575. skipConfirm := cmd.Bool("yes")
  576. if !skipConfirm {
  577. confirm := false
  578. prompt := &survey.Confirm{
  579. Message: fmt.Sprintf("Are you sure you want to delete workflow template %s?", id),
  580. Default: false,
  581. }
  582. if err := survey.AskOne(prompt, &confirm); err != nil {
  583. return err
  584. }
  585. if !confirm {
  586. fmt.Println("Deletion cancelled.")
  587. return nil
  588. }
  589. }
  590. mutation := `mutation DeleteWorkflowTemplate($id: ID!) { deleteWorkflowTemplate(id: $id) }`
  591. resp, err := c.Mutation(mutation, map[string]interface{}{"id": id})
  592. if err != nil {
  593. return err
  594. }
  595. var result struct {
  596. DeleteWorkflowTemplate bool `json:"deleteWorkflowTemplate"`
  597. }
  598. if err := json.Unmarshal(resp.Data, &result); err != nil {
  599. return err
  600. }
  601. if result.DeleteWorkflowTemplate {
  602. fmt.Printf("Workflow template %s deleted successfully.\n", id)
  603. } else {
  604. fmt.Printf("Failed to delete workflow template %s.\n", id)
  605. }
  606. return nil
  607. }
  608. func workflowTemplateExample(ctx context.Context, cmd *cli.Command) error {
  609. outputPath := cmd.String("output")
  610. // Example template content
  611. exampleTemplate := WorkflowTemplateFile{
  612. Name: "Standard Approval Workflow",
  613. Description: "A simple linear approval workflow for testing",
  614. Definition: map[string]interface{}{
  615. "nodes": map[string]interface{}{
  616. "start": map[string]interface{}{
  617. "type": "task",
  618. "title": "Initial Review",
  619. "content": "Review the incoming request",
  620. "assignee": "1",
  621. "dependsOn": []interface{}{},
  622. },
  623. "analysis": map[string]interface{}{
  624. "type": "task",
  625. "title": "Data Analysis",
  626. "content": "Analyze the data",
  627. "assignee": "2",
  628. "dependsOn": []interface{}{"start"},
  629. },
  630. "approval": map[string]interface{}{
  631. "type": "task",
  632. "title": "Manager Approval",
  633. "content": "Approve the request",
  634. "assignee": "3",
  635. "dependsOn": []interface{}{"analysis"},
  636. },
  637. "complete": map[string]interface{}{
  638. "type": "task",
  639. "title": "Complete",
  640. "content": "Finalize",
  641. "assignee": "1",
  642. "dependsOn": []interface{}{"approval"},
  643. },
  644. },
  645. },
  646. IsActive: true,
  647. }
  648. // Marshal to JSON with indentation
  649. data, err := json.MarshalIndent(exampleTemplate, "", " ")
  650. if err != nil {
  651. return fmt.Errorf("failed to marshal example template: %w", err)
  652. }
  653. // Write to file
  654. err = os.WriteFile(outputPath, data, 0644)
  655. if err != nil {
  656. return fmt.Errorf("failed to write example file: %w", err)
  657. }
  658. fmt.Printf("Example workflow template written to: %s\n", outputPath)
  659. fmt.Println("Edit the file to customize your workflow, then use:")
  660. fmt.Printf(" arp_cli workflow template create --file %s\n", outputPath)
  661. return nil
  662. }
  663. // WorkflowInstance operations
  664. func workflowInstanceList(ctx context.Context, cmd *cli.Command) error {
  665. c, cfg, err := GetClient(ctx, cmd)
  666. if err != nil {
  667. return err
  668. }
  669. if err := RequireAuth(cfg); err != nil {
  670. return err
  671. }
  672. query := `query WorkflowInstances { workflowInstances { id template { id name } status context service { id name } createdAt updatedAt completedAt } }`
  673. resp, err := c.Query(query, nil)
  674. if err != nil {
  675. return err
  676. }
  677. var result struct {
  678. WorkflowInstances []WorkflowInstance `json:"workflowInstances"`
  679. }
  680. if err := json.Unmarshal(resp.Data, &result); err != nil {
  681. return err
  682. }
  683. if cmd.Bool("json") {
  684. enc := json.NewEncoder(os.Stdout)
  685. enc.SetIndent("", " ")
  686. return enc.Encode(result.WorkflowInstances)
  687. }
  688. if len(result.WorkflowInstances) == 0 {
  689. fmt.Println("No workflow instances found.")
  690. return nil
  691. }
  692. table := tablewriter.NewWriter(os.Stdout)
  693. table.Header([]string{"ID", "Template", "Status", "Service", "Created At"})
  694. for _, i := range result.WorkflowInstances {
  695. templateName := ""
  696. if i.Template != nil {
  697. templateName = i.Template.Name
  698. }
  699. serviceName := ""
  700. if i.Service != nil {
  701. serviceName = i.Service.Name
  702. }
  703. table.Append([]string{i.ID, templateName, i.Status, serviceName, i.CreatedAt})
  704. }
  705. table.Render()
  706. return nil
  707. }
  708. func workflowInstanceGet(ctx context.Context, cmd *cli.Command) error {
  709. c, cfg, err := GetClient(ctx, cmd)
  710. if err != nil {
  711. return err
  712. }
  713. if err := RequireAuth(cfg); err != nil {
  714. return err
  715. }
  716. id := cmd.String("id")
  717. query := `query WorkflowInstance($id: ID!) { workflowInstance(id: $id) { id template { id name description } status context service { id name } createdAt updatedAt completedAt } }`
  718. resp, err := c.Query(query, map[string]interface{}{"id": id})
  719. if err != nil {
  720. return err
  721. }
  722. var result struct {
  723. WorkflowInstance *WorkflowInstance `json:"workflowInstance"`
  724. }
  725. if err := json.Unmarshal(resp.Data, &result); err != nil {
  726. return err
  727. }
  728. if result.WorkflowInstance == nil {
  729. return fmt.Errorf("workflow instance not found")
  730. }
  731. if cmd.Bool("json") {
  732. enc := json.NewEncoder(os.Stdout)
  733. enc.SetIndent("", " ")
  734. return enc.Encode(result.WorkflowInstance)
  735. }
  736. i := result.WorkflowInstance
  737. fmt.Printf("ID: %s\n", i.ID)
  738. if i.Template != nil {
  739. fmt.Printf("Template: %s (%s)\n", i.Template.Name, i.Template.ID)
  740. }
  741. fmt.Printf("Status: %s\n", i.Status)
  742. if i.Service != nil {
  743. fmt.Printf("Service: %s (%s)\n", i.Service.Name, i.Service.ID)
  744. }
  745. fmt.Printf("Created At: %s\n", i.CreatedAt)
  746. fmt.Printf("Updated At: %s\n", i.UpdatedAt)
  747. if i.CompletedAt != nil {
  748. fmt.Printf("Completed At: %s\n", *i.CompletedAt)
  749. }
  750. if i.Context != "" {
  751. fmt.Printf("\nContext:\n%s\n", i.Context)
  752. }
  753. return nil
  754. }
  755. func workflowInstanceStart(ctx context.Context, cmd *cli.Command) error {
  756. c, cfg, err := GetClient(ctx, cmd)
  757. if err != nil {
  758. return err
  759. }
  760. if err := RequireAuth(cfg); err != nil {
  761. return err
  762. }
  763. templateID := cmd.String("template")
  764. serviceID := cmd.String("service")
  765. contextJSON := cmd.String("context")
  766. mutation := `mutation StartWorkflow($templateId: ID!, $input: StartWorkflowInput!) { startWorkflow(templateId: $templateId, input: $input) { id template { id name } status context service { id name } createdAt } }`
  767. input := make(map[string]interface{})
  768. if serviceID != "" {
  769. input["serviceId"] = serviceID
  770. }
  771. if contextJSON != "" {
  772. input["context"] = contextJSON
  773. }
  774. resp, err := c.Mutation(mutation, map[string]interface{}{"templateId": templateID, "input": input})
  775. if err != nil {
  776. return err
  777. }
  778. var result struct {
  779. StartWorkflow *WorkflowInstance `json:"startWorkflow"`
  780. }
  781. if err := json.Unmarshal(resp.Data, &result); err != nil {
  782. return err
  783. }
  784. if result.StartWorkflow == nil {
  785. return fmt.Errorf("failed to start workflow")
  786. }
  787. fmt.Printf("Workflow started successfully!\n")
  788. fmt.Printf("Instance ID: %s\n", result.StartWorkflow.ID)
  789. if result.StartWorkflow.Template != nil {
  790. fmt.Printf("Template: %s\n", result.StartWorkflow.Template.Name)
  791. }
  792. fmt.Printf("Status: %s\n", result.StartWorkflow.Status)
  793. return nil
  794. }
  795. func workflowInstanceCancel(ctx context.Context, cmd *cli.Command) error {
  796. c, cfg, err := GetClient(ctx, cmd)
  797. if err != nil {
  798. return err
  799. }
  800. if err := RequireAuth(cfg); err != nil {
  801. return err
  802. }
  803. id := cmd.String("id")
  804. mutation := `mutation CancelWorkflow($id: ID!) { cancelWorkflow(id: $id) { id status completedAt } }`
  805. resp, err := c.Mutation(mutation, map[string]interface{}{"id": id})
  806. if err != nil {
  807. return err
  808. }
  809. var result struct {
  810. CancelWorkflow *WorkflowInstance `json:"cancelWorkflow"`
  811. }
  812. if err := json.Unmarshal(resp.Data, &result); err != nil {
  813. return err
  814. }
  815. if result.CancelWorkflow == nil {
  816. return fmt.Errorf("workflow instance not found")
  817. }
  818. fmt.Printf("Workflow cancelled successfully!\n")
  819. fmt.Printf("Instance ID: %s\n", result.CancelWorkflow.ID)
  820. fmt.Printf("Status: %s\n", result.CancelWorkflow.Status)
  821. return nil
  822. }
  823. // WorkflowNode operations
  824. func workflowNodeList(ctx context.Context, cmd *cli.Command) error {
  825. c, cfg, err := GetClient(ctx, cmd)
  826. if err != nil {
  827. return err
  828. }
  829. if err := RequireAuth(cfg); err != nil {
  830. return err
  831. }
  832. instanceID := cmd.String("instance")
  833. query := `query WorkflowInstance($id: ID!) { workflowInstance(id: $id) { id status nodes: workflowNodes { id nodeKey nodeType status task { id title } retryCount createdAt startedAt completedAt } } }`
  834. resp, err := c.Query(query, map[string]interface{}{"id": instanceID})
  835. if err != nil {
  836. return err
  837. }
  838. var result struct {
  839. WorkflowInstance *struct {
  840. ID string `json:"id"`
  841. Status string `json:"status"`
  842. Nodes []WorkflowNode `json:"nodes"`
  843. } `json:"workflowInstance"`
  844. }
  845. if err := json.Unmarshal(resp.Data, &result); err != nil {
  846. return err
  847. }
  848. if result.WorkflowInstance == nil {
  849. return fmt.Errorf("workflow instance not found")
  850. }
  851. if cmd.Bool("json") {
  852. enc := json.NewEncoder(os.Stdout)
  853. enc.SetIndent("", " ")
  854. return enc.Encode(result.WorkflowInstance.Nodes)
  855. }
  856. if len(result.WorkflowInstance.Nodes) == 0 {
  857. fmt.Println("No nodes found for this workflow instance.")
  858. return nil
  859. }
  860. table := tablewriter.NewWriter(os.Stdout)
  861. table.Header([]string{"ID", "Key", "Type", "Status", "Task", "Retries"})
  862. for _, n := range result.WorkflowInstance.Nodes {
  863. taskTitle := ""
  864. if n.Task != nil {
  865. taskTitle = n.Task.Title
  866. if len(taskTitle) > 30 {
  867. taskTitle = taskTitle[:27] + "..."
  868. }
  869. }
  870. table.Append([]string{n.ID, n.NodeKey, n.NodeType, n.Status, taskTitle, fmt.Sprintf("%d", n.RetryCount)})
  871. }
  872. table.Render()
  873. return nil
  874. }
  875. func workflowNodeGet(ctx context.Context, cmd *cli.Command) error {
  876. c, cfg, err := GetClient(ctx, cmd)
  877. if err != nil {
  878. return err
  879. }
  880. if err := RequireAuth(cfg); err != nil {
  881. return err
  882. }
  883. id := cmd.String("id")
  884. query := `query WorkflowNode($id: ID!) { workflowNode(id: $id) { id nodeKey nodeType status task { id title } inputData outputData retryCount createdAt updatedAt startedAt completedAt } }`
  885. // Note: This assumes a workflowNode query exists. If not, we need to fetch via instance
  886. // For now, let's use a workaround by fetching the instance and finding the node
  887. query = `query WorkflowInstances { workflowInstances { id nodes: workflowNodes { id nodeKey nodeType status task { id title content } inputData outputData retryCount createdAt updatedAt startedAt completedAt } } }`
  888. resp, err := c.Query(query, nil)
  889. if err != nil {
  890. return err
  891. }
  892. var result struct {
  893. WorkflowInstances []struct {
  894. ID string `json:"id"`
  895. Nodes []WorkflowNode `json:"nodes"`
  896. } `json:"workflowInstances"`
  897. }
  898. if err := json.Unmarshal(resp.Data, &result); err != nil {
  899. return err
  900. }
  901. // Find the node
  902. var node *WorkflowNode
  903. for _, instance := range result.WorkflowInstances {
  904. for _, n := range instance.Nodes {
  905. if n.ID == id {
  906. node = &n
  907. break
  908. }
  909. }
  910. if node != nil {
  911. break
  912. }
  913. }
  914. if node == nil {
  915. return fmt.Errorf("workflow node not found")
  916. }
  917. if cmd.Bool("json") {
  918. enc := json.NewEncoder(os.Stdout)
  919. enc.SetIndent("", " ")
  920. return enc.Encode(node)
  921. }
  922. fmt.Printf("ID: %s\n", node.ID)
  923. fmt.Printf("Key: %s\n", node.NodeKey)
  924. fmt.Printf("Type: %s\n", node.NodeType)
  925. fmt.Printf("Status: %s\n", node.Status)
  926. fmt.Printf("Retry Count: %d\n", node.RetryCount)
  927. if node.Task != nil {
  928. fmt.Printf("Task: %s (%s)\n", node.Task.Title, node.Task.ID)
  929. }
  930. fmt.Printf("Created At: %s\n", node.CreatedAt)
  931. fmt.Printf("Updated At: %s\n", node.UpdatedAt)
  932. if node.StartedAt != nil {
  933. fmt.Printf("Started At: %s\n", *node.StartedAt)
  934. }
  935. if node.CompletedAt != nil {
  936. fmt.Printf("Completed At: %s\n", *node.CompletedAt)
  937. }
  938. if node.InputData != "" {
  939. fmt.Printf("\nInput Data:\n%s\n", node.InputData)
  940. }
  941. if node.OutputData != "" {
  942. fmt.Printf("\nOutput Data:\n%s\n", node.OutputData)
  943. }
  944. return nil
  945. }
  946. func workflowNodeRetry(ctx context.Context, cmd *cli.Command) error {
  947. c, cfg, err := GetClient(ctx, cmd)
  948. if err != nil {
  949. return err
  950. }
  951. if err := RequireAuth(cfg); err != nil {
  952. return err
  953. }
  954. id := cmd.String("id")
  955. mutation := `mutation RetryWorkflowNode($nodeId: ID!) { retryWorkflowNode(nodeId: $nodeId) { id nodeKey status retryCount } }`
  956. resp, err := c.Mutation(mutation, map[string]interface{}{"nodeId": id})
  957. if err != nil {
  958. return err
  959. }
  960. var result struct {
  961. RetryWorkflowNode *WorkflowNode `json:"retryWorkflowNode"`
  962. }
  963. if err := json.Unmarshal(resp.Data, &result); err != nil {
  964. return err
  965. }
  966. if result.RetryWorkflowNode == nil {
  967. return fmt.Errorf("workflow node not found")
  968. }
  969. fmt.Printf("Workflow node retry initiated!\n")
  970. fmt.Printf("Node ID: %s\n", result.RetryWorkflowNode.ID)
  971. fmt.Printf("Key: %s\n", result.RetryWorkflowNode.NodeKey)
  972. fmt.Printf("Status: %s\n", result.RetryWorkflowNode.Status)
  973. fmt.Printf("Retry Count: %d\n", result.RetryWorkflowNode.RetryCount)
  974. return nil
  975. }
  976. // ReadFileOrString reads file content if input starts with @, otherwise returns as-is
  977. func ReadFileOrString(input string) string {
  978. if strings.HasPrefix(input, "@") {
  979. filename := input[1:]
  980. content, err := os.ReadFile(filename)
  981. if err != nil {
  982. return input // Return original if file can't be read
  983. }
  984. return string(content)
  985. }
  986. return input
  987. }