diff --git a/cmd/workflow_approve.go b/cmd/workflow_approve.go new file mode 100644 index 0000000..77ab0bb --- /dev/null +++ b/cmd/workflow_approve.go @@ -0,0 +1,76 @@ +package cmd + +import ( + "fmt" + "strings" + + "github.com/nudgebee/nbctl/pkg/client" + "github.com/nudgebee/nbctl/pkg/format" + "github.com/spf13/cobra" +) + +var workflowApproveCmd = &cobra.Command{ + Use: "approve ", + Short: "Complete a human approval gate for a pending workflow execution", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + executionID := strings.TrimSpace(args[0]) + if executionID == "" { + return fmt.Errorf("execution-id cannot be empty") + } + taskID, _ := cmd.Flags().GetString("task") + taskID = strings.TrimSpace(taskID) + if taskID == "" { + return fmt.Errorf("task flag is required and cannot be empty") + } + + reject, _ := cmd.Flags().GetBool("reject") + comments, _ := cmd.Flags().GetString("comments") + comments = strings.TrimSpace(comments) + + status := "approved" + if reject { + status = "rejected" + } + + req := client.NewRequest(` + mutation CompleteWorkflowApproval($request: WorkflowCompleteApprovalRequest!) { + workflow_complete_approval(request: $request) { + status + message + } + } + `) + input := map[string]any{ + "execution_id": executionID, + "task_id": taskID, + "status": status, + } + if comments != "" { + input["comments"] = comments + } + req.Var("request", input) + + var respData struct { + WorkflowCompleteApproval struct { + Status string `json:"status"` + Message string `json:"message"` + } `json:"workflow_complete_approval"` + } + + if err := client.Run(cmd.Context(), req, &respData); err != nil { + return err + } + + format.GetFormat().Print(respData.WorkflowCompleteApproval) + return nil + }, +} + +func init() { + workflowCmd.AddCommand(workflowApproveCmd) + workflowApproveCmd.Flags().String("task", "", "Task ID waiting for approval (required)") + workflowApproveCmd.Flags().Bool("reject", false, "Reject the approval step instead of approving") + workflowApproveCmd.Flags().String("comments", "", "Optional comments for the approval decision") + _ = workflowApproveCmd.MarkFlagRequired("task") +} diff --git a/cmd/workflow_replay.go b/cmd/workflow_replay.go new file mode 100644 index 0000000..0a66a3e --- /dev/null +++ b/cmd/workflow_replay.go @@ -0,0 +1,53 @@ +package cmd + +import ( + "fmt" + "strings" + + "github.com/nudgebee/nbctl/pkg/client" + "github.com/nudgebee/nbctl/pkg/format" + "github.com/spf13/cobra" +) + +var workflowReplayCmd = &cobra.Command{ + Use: "replay ", + Short: "Replay a previous or failed workflow execution", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + executionID := strings.TrimSpace(args[0]) + if executionID == "" { + return fmt.Errorf("execution-id cannot be empty") + } + req := client.NewRequest(` + mutation ReplayWorkflowExecution($request: WorkflowRetriggerRequest!) { + workflow_replay_execution(request: $request) { + execution_id + status + message + } + } + `) + req.Var("request", map[string]any{ + "execution_id": executionID, + }) + + var respData struct { + WorkflowReplayExecution struct { + ExecutionID string `json:"execution_id"` + Status string `json:"status"` + Message string `json:"message"` + } `json:"workflow_replay_execution"` + } + + if err := client.Run(cmd.Context(), req, &respData); err != nil { + return err + } + + format.GetFormat().Print(respData.WorkflowReplayExecution) + return nil + }, +} + +func init() { + workflowCmd.AddCommand(workflowReplayCmd) +} diff --git a/cmd/workflow_task_definitions.go b/cmd/workflow_task_definitions.go new file mode 100644 index 0000000..e55bd79 --- /dev/null +++ b/cmd/workflow_task_definitions.go @@ -0,0 +1,90 @@ +package cmd + +import ( + "strings" + + "github.com/nudgebee/nbctl/pkg/client" + "github.com/nudgebee/nbctl/pkg/format" + "github.com/spf13/cobra" +) + +var workflowTaskDefinitionsCmd = &cobra.Command{ + Use: "task-definitions", + Short: "List supported workflow task definitions and action schemas", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + nameFilter, _ := cmd.Flags().GetString("name") + nameFilter = strings.TrimSpace(nameFilter) + limit, _ := cmd.Flags().GetInt("limit") + + req := client.NewRequest(` + query ListWorkflowTaskDefinitions($params: WorkflowTaskDefinitionListRequest!) { + workflow_list_taskdefinitions(params: $params) { + tasks { + name + description + aliases + } + } + } + `) + + params := map[string]any{ + "limit": limit, + } + if nameFilter != "" { + params["name"] = nameFilter + } + req.Var("params", params) + + var respData struct { + WorkflowListTaskdefinitions struct { + Tasks []struct { + Name string `json:"name"` + Description string `json:"description"` + Aliases []string `json:"aliases"` + } `json:"tasks"` + } `json:"workflow_list_taskdefinitions"` + } + + if err := client.Run(cmd.Context(), req, &respData); err != nil { + return err + } + + type taskRow struct { + Name string `json:"name"` + Description string `json:"description"` + Aliases string `json:"aliases"` + } + rows := make([]taskRow, 0, len(respData.WorkflowListTaskdefinitions.Tasks)) + for _, t := range respData.WorkflowListTaskdefinitions.Tasks { + aliasesStr := "-" + if len(t.Aliases) > 0 { + aliasesStr = strings.Join(t.Aliases, ", ") + } + rows = append(rows, taskRow{ + Name: t.Name, + Description: t.Description, + Aliases: aliasesStr, + }) + } + + table := format.TabularData{ + Data: rows, + Fields: []format.TableField{ + {Header: "Task Name", Field: "Name"}, + {Header: "Description", Field: "Description"}, + {Header: "Aliases", Field: "Aliases"}, + }, + } + format.GetFormat().Print(table) + + return nil + }, +} + +func init() { + workflowCmd.AddCommand(workflowTaskDefinitionsCmd) + workflowTaskDefinitionsCmd.Flags().String("name", "", "Filter task definitions by name") + workflowTaskDefinitionsCmd.Flags().Int("limit", 100, "Maximum number of task definitions to return") +} diff --git a/cmd/workflow_templates.go b/cmd/workflow_templates.go new file mode 100644 index 0000000..d604669 --- /dev/null +++ b/cmd/workflow_templates.go @@ -0,0 +1,158 @@ +package cmd + +import ( + "fmt" + "strings" + + "github.com/nudgebee/nbctl/pkg/client" + "github.com/nudgebee/nbctl/pkg/format" + "github.com/spf13/cobra" +) + +type workflowTemplate struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Category string `json:"category"` + IsSystem bool `json:"is_system"` + Status string `json:"status"` +} + +var workflowTemplatesCmd = &cobra.Command{ + Use: "templates", + Short: "Browse and inspect pre-built workflow templates", +} + +var workflowTemplatesListCmd = &cobra.Command{ + Use: "list", + Short: "List pre-built workflow templates", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + typeFlag, _ := cmd.Flags().GetString("type") + typeFlag = strings.ToLower(typeFlag) + category, _ := cmd.Flags().GetString("category") + category = strings.TrimSpace(category) + limit, _ := cmd.Flags().GetInt("limit") + + if typeFlag != "system" && typeFlag != "custom" && typeFlag != "all" { + return fmt.Errorf("invalid template type '%s': must be 'system', 'custom', or 'all'", typeFlag) + } + + req := client.NewRequest(` + query ListWorkflowTemplates($request: WorkflowListTemplateRequest!) { + workflow_list_template(request: $request) { + total_count + templates { + id + name + description + category + is_system + status + } + } + } + `) + + input := map[string]any{ + "type": typeFlag, + "limit": limit, + } + if category != "" { + input["category"] = category + } + req.Var("request", input) + + var respData struct { + WorkflowListTemplate struct { + TotalCount int `json:"total_count"` + Templates []workflowTemplate `json:"templates"` + } `json:"workflow_list_template"` + } + + if err := client.Run(cmd.Context(), req, &respData); err != nil { + return err + } + + table := format.TabularData{ + Data: respData.WorkflowListTemplate.Templates, + Fields: []format.TableField{ + {Header: "Template ID", Field: "ID"}, + {Header: "Template Name", Field: "Name"}, + {Header: "Category", Field: "Category"}, + {Header: "System", Field: "IsSystem"}, + {Header: "Description", Field: "Description"}, + }, + } + format.GetFormat().Print(table) + + return nil + }, +} + +var workflowTemplatesGetCmd = &cobra.Command{ + Use: "get ", + Short: "Get details for a specific workflow template", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + templateID := strings.TrimSpace(args[0]) + if templateID == "" { + return fmt.Errorf("template-id cannot be empty") + } + typeFlag, _ := cmd.Flags().GetString("type") + typeFlag = strings.ToLower(typeFlag) + + if typeFlag != "" && typeFlag != "system" && typeFlag != "custom" && typeFlag != "all" { + return fmt.Errorf("invalid template type '%s': must be 'system', 'custom', or 'all'", typeFlag) + } + + typeVal := typeFlag + if typeVal == "" { + typeVal = "all" + } + + req := client.NewRequest(` + query GetWorkflowTemplate($request: WorkflowGetTemplateRequest!) { + workflow_get_template(request: $request) { + id + name + description + category + is_system + status + } + } + `) + req.Var("request", map[string]any{ + "type": typeVal, + "id": templateID, + }) + + var respData struct { + WorkflowGetTemplate *workflowTemplate `json:"workflow_get_template"` + } + + if err := client.Run(cmd.Context(), req, &respData); err != nil { + return err + } + + if respData.WorkflowGetTemplate == nil || respData.WorkflowGetTemplate.ID == "" { + return fmt.Errorf("workflow template '%s' not found", templateID) + } + + format.GetFormat().Print(*respData.WorkflowGetTemplate) + return nil + }, +} + +func init() { + workflowCmd.AddCommand(workflowTemplatesCmd) + workflowTemplatesCmd.AddCommand(workflowTemplatesListCmd) + workflowTemplatesCmd.AddCommand(workflowTemplatesGetCmd) + + workflowTemplatesListCmd.Flags().String("type", "system", "Template type (system, custom, or all)") + workflowTemplatesListCmd.Flags().String("category", "", "Filter templates by category") + workflowTemplatesListCmd.Flags().Int("limit", 50, "Maximum number of templates to return") + + workflowTemplatesGetCmd.Flags().String("type", "", "Optional template type filter (system, custom, or all)") +}