diff --git a/pkg/cli/add_command.go b/pkg/cli/add_command.go index 326386a763..4e5e18a0fe 100644 --- a/pkg/cli/add_command.go +++ b/pkg/cli/add_command.go @@ -296,6 +296,68 @@ func handleRepoOnlySpec(repoSpec string, verbose bool) error { return nil } +// displayAvailableWorkflows lists available workflows from an installed package +func displayAvailableWorkflows(repoSlug, version string, verbose bool) error { + addLog.Printf("Displaying available workflows for repository: %s", repoSlug) + + // List workflows in the installed package + workflows, err := listWorkflowsInPackage(repoSlug, verbose) + if err != nil { + return fmt.Errorf("failed to list workflows in %s: %w", repoSlug, err) + } + + // Display the list of available workflows + if len(workflows) == 0 { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("No workflows found in repository %s", repoSlug))) + return nil + } + + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Available workflows in %s:", repoSlug))) + fmt.Fprintln(os.Stderr, "") + + for _, workflow := range workflows { + // Extract workflow name from path (remove .md extension and path) + workflowName := strings.TrimSuffix(filepath.Base(workflow), ".md") + + // For workflows in workflows/ directory, show simplified name + if strings.HasPrefix(workflow, "workflows/") { + workflowName = strings.TrimSuffix(strings.TrimPrefix(workflow, "workflows/"), ".md") + } + + // Build the full command + fullSpec := fmt.Sprintf("%s/%s", repoSlug, workflowName) + if version != "" { + fullSpec += "@" + version + } + + fmt.Fprintf(os.Stderr, " • %s\n", workflowName) + if verbose { + fmt.Fprintf(os.Stderr, " Command: %s add %s\n", constants.CLIExtensionPrefix, fullSpec) + } + } + + fmt.Fprintln(os.Stderr, "") + fmt.Fprintf(os.Stderr, "Example:\n") + + // Show example with first workflow + exampleWorkflow := workflows[0] + exampleName := strings.TrimSuffix(filepath.Base(exampleWorkflow), ".md") + if strings.HasPrefix(exampleWorkflow, "workflows/") { + exampleName = strings.TrimSuffix(strings.TrimPrefix(exampleWorkflow, "workflows/"), ".md") + } + + exampleSpec := fmt.Sprintf("%s/%s", repoSlug, exampleName) + if version != "" { + exampleSpec += "@" + version + } + + fmt.Fprintf(os.Stderr, " %s add %s\n", constants.CLIExtensionPrefix, exampleSpec) + fmt.Fprintln(os.Stderr, "") + + return nil +} + // addWorkflowsNormal handles normal workflow addition without PR creation func addWorkflowsNormal(workflows []*WorkflowSpec, number int, verbose bool, engineOverride string, name string, force bool, appendText string, noGitattributes bool) error { // Create file tracker for all operations @@ -488,15 +550,18 @@ func addWorkflowWithTracking(workflow *WorkflowSpec, number int, verbose bool, e if err != nil { fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("Workflow '%s' not found.", workflowPath))) - // Provide information about workflow repositories - fmt.Println("\nTo add workflows to your project:") - fmt.Println("=================================") - fmt.Println("Use the 'add' command with repository/workflow specifications:") - fmt.Println(" " + constants.CLIExtensionPrefix + " add owner/repo/workflow-name") - fmt.Println(" " + constants.CLIExtensionPrefix + " add owner/repo/workflow-name@version") - fmt.Println("\nExample:") - fmt.Println(" " + constants.CLIExtensionPrefix + " add githubnext/agentics/ci-doctor") - fmt.Println(" " + constants.CLIExtensionPrefix + " add githubnext/agentics/daily-plan@main") + // Try to list available workflows from the installed package + if err := displayAvailableWorkflows(workflow.RepoSlug, workflow.Version, verbose); err != nil { + // If we can't list workflows, provide generic help + fmt.Println("\nTo add workflows to your project:") + fmt.Println("=================================") + fmt.Println("Use the 'add' command with repository/workflow specifications:") + fmt.Println(" " + constants.CLIExtensionPrefix + " add owner/repo/workflow-name") + fmt.Println(" " + constants.CLIExtensionPrefix + " add owner/repo/workflow-name@version") + fmt.Println("\nExample:") + fmt.Println(" " + constants.CLIExtensionPrefix + " add githubnext/agentics/ci-doctor") + fmt.Println(" " + constants.CLIExtensionPrefix + " add githubnext/agentics/daily-plan@main") + } return fmt.Errorf("workflow not found: %s", workflowPath) } diff --git a/pkg/cli/add_workflow_not_found_test.go b/pkg/cli/add_workflow_not_found_test.go new file mode 100644 index 0000000000..77298577ac --- /dev/null +++ b/pkg/cli/add_workflow_not_found_test.go @@ -0,0 +1,207 @@ +package cli + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" +) + +// TestDisplayAvailableWorkflows tests that displayAvailableWorkflows shows the list of available workflows +func TestDisplayAvailableWorkflows(t *testing.T) { + // Create a temporary packages directory structure + tempDir := t.TempDir() + + // Override packages directory for testing + oldHome := os.Getenv("HOME") + os.Setenv("HOME", tempDir) + defer os.Setenv("HOME", oldHome) + + // Create a mock package structure + packagePath := filepath.Join(tempDir, ".aw", "packages", "test-owner", "test-repo") + workflowsDir := filepath.Join(packagePath, "workflows") + if err := os.MkdirAll(workflowsDir, 0755); err != nil { + t.Fatalf("Failed to create test directories: %v", err) + } + + // Create some mock workflow files with valid frontmatter + validWorkflowContent := `--- +on: push +--- + +# Test Workflow +` + + workflows := []string{ + "ci-doctor.md", + "daily-plan.md", + "weekly-summary.md", + } + + for _, wf := range workflows { + wfPath := filepath.Join(workflowsDir, wf) + if err := os.WriteFile(wfPath, []byte(validWorkflowContent), 0644); err != nil { + t.Fatalf("Failed to create workflow file %s: %v", wf, err) + } + } + + // Capture stderr output + oldStderr := os.Stderr + r, w, _ := os.Pipe() + os.Stderr = w + + // Call displayAvailableWorkflows + err := displayAvailableWorkflows("test-owner/test-repo", "", false) + + // Restore stderr and capture output + w.Close() + os.Stderr = oldStderr + var buf bytes.Buffer + buf.ReadFrom(r) + output := buf.String() + + if err != nil { + t.Errorf("displayAvailableWorkflows() unexpected error: %v", err) + } + + // Check that the output contains the expected workflow names + expectedWorkflows := []string{"ci-doctor", "daily-plan", "weekly-summary"} + for _, wf := range expectedWorkflows { + if !strings.Contains(output, wf) { + t.Errorf("displayAvailableWorkflows() output should contain workflow '%s', got:\n%s", wf, output) + } + } + + // Check that the output contains helpful information + if !strings.Contains(output, "Available workflows") { + t.Errorf("displayAvailableWorkflows() output should contain 'Available workflows', got:\n%s", output) + } + + if !strings.Contains(output, "Example:") { + t.Errorf("displayAvailableWorkflows() output should contain 'Example:', got:\n%s", output) + } +} + +// TestDisplayAvailableWorkflowsWithVersion tests displayAvailableWorkflows with a version +func TestDisplayAvailableWorkflowsWithVersion(t *testing.T) { + // Create a temporary packages directory structure + tempDir := t.TempDir() + + // Override packages directory for testing + oldHome := os.Getenv("HOME") + os.Setenv("HOME", tempDir) + defer os.Setenv("HOME", oldHome) + + // Create a mock package structure + packagePath := filepath.Join(tempDir, ".aw", "packages", "test-owner", "test-repo") + workflowsDir := filepath.Join(packagePath, "workflows") + if err := os.MkdirAll(workflowsDir, 0755); err != nil { + t.Fatalf("Failed to create test directories: %v", err) + } + + // Create a mock workflow file + validWorkflowContent := `--- +on: push +--- + +# Test Workflow +` + wfPath := filepath.Join(workflowsDir, "test-workflow.md") + if err := os.WriteFile(wfPath, []byte(validWorkflowContent), 0644); err != nil { + t.Fatalf("Failed to create workflow file: %v", err) + } + + // Capture stderr output + oldStderr := os.Stderr + r, w, _ := os.Pipe() + os.Stderr = w + + // Call displayAvailableWorkflows with a version + err := displayAvailableWorkflows("test-owner/test-repo", "v1.0.0", false) + + // Restore stderr and capture output + w.Close() + os.Stderr = oldStderr + var buf bytes.Buffer + buf.ReadFrom(r) + output := buf.String() + + if err != nil { + t.Errorf("displayAvailableWorkflows() unexpected error: %v", err) + } + + // Check that the version is included in the example command + if !strings.Contains(output, "@v1.0.0") { + t.Errorf("displayAvailableWorkflows() output should include version '@v1.0.0', got:\n%s", output) + } +} + +// TestDisplayAvailableWorkflowsNoWorkflows tests when no workflows are found +func TestDisplayAvailableWorkflowsNoWorkflows(t *testing.T) { + // Create a temporary packages directory structure + tempDir := t.TempDir() + + // Override packages directory for testing + oldHome := os.Getenv("HOME") + os.Setenv("HOME", tempDir) + defer os.Setenv("HOME", oldHome) + + // Create an empty package structure + packagePath := filepath.Join(tempDir, ".aw", "packages", "test-owner", "test-repo") + if err := os.MkdirAll(packagePath, 0755); err != nil { + t.Fatalf("Failed to create test directories: %v", err) + } + + // Capture stderr output + oldStderr := os.Stderr + r, w, _ := os.Pipe() + os.Stderr = w + + // Call displayAvailableWorkflows + err := displayAvailableWorkflows("test-owner/test-repo", "", false) + + // Restore stderr and capture output + w.Close() + os.Stderr = oldStderr + var buf bytes.Buffer + buf.ReadFrom(r) + output := buf.String() + + if err != nil { + t.Errorf("displayAvailableWorkflows() unexpected error: %v", err) + } + + // Check that the output contains a warning about no workflows + if !strings.Contains(output, "No workflows found") { + t.Errorf("displayAvailableWorkflows() output should contain 'No workflows found', got:\n%s", output) + } +} + +// TestDisplayAvailableWorkflowsPackageNotFound tests when package is not found +func TestDisplayAvailableWorkflowsPackageNotFound(t *testing.T) { + // Create a temporary packages directory + tempDir := t.TempDir() + + // Override packages directory for testing + oldHome := os.Getenv("HOME") + os.Setenv("HOME", tempDir) + defer os.Setenv("HOME", oldHome) + + // Create packages directory but don't create the specific package + packagesDir := filepath.Join(tempDir, ".aw", "packages") + if err := os.MkdirAll(packagesDir, 0755); err != nil { + t.Fatalf("Failed to create packages directory: %v", err) + } + + // Call displayAvailableWorkflows with non-existent package + err := displayAvailableWorkflows("nonexistent/repo", "", false) + + if err == nil { + t.Error("displayAvailableWorkflows() expected error for non-existent package, got nil") + } + + if !strings.Contains(err.Error(), "package not found") { + t.Errorf("displayAvailableWorkflows() error should contain 'package not found', got: %v", err) + } +}