Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 101 additions & 1 deletion cmd/af/gen/prompts/prompts.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,33 @@ limitations under the License.
package prompts

import (
"bufio"
"fmt"
"io"
"io/fs"
"log/slog"
"os"
"path/filepath"
"strconv"
"strings"

"github.com/omniaura/agentflow/pkg/assert"
"github.com/omniaura/agentflow/pkg/ast"
"github.com/omniaura/agentflow/pkg/gen/gogen"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
"golang.org/x/term"
)

var (
Dir string

interactiveInput io.Reader = os.Stdin
interactiveOutput io.Writer = os.Stdout
isInteractiveTTY = func() bool {
file, ok := interactiveInput.(*os.File)
return ok && term.IsTerminal(int(file.Fd()))
}
)

func flags(cmd *cobra.Command) *cobra.Command {
Expand All @@ -47,7 +59,7 @@ func CMD() *cobra.Command {
The generated prompts will be written next to their corresponding .af files.`,
Run: func(cmd *cobra.Command, args []string) {
ctx := cmd.Context()
files, err := collectAFFiles(Dir)
files, err := resolveAFFiles(cmd)
assert.NoError(err)

group, _ := errgroup.WithContext(ctx)
Expand Down Expand Up @@ -131,3 +143,91 @@ func collectAFFiles(dir string) ([]string, error) {

return files, err
}

func resolveAFFiles(cmd *cobra.Command) ([]string, error) {
files, err := collectAFFiles(Dir)
if err != nil {
return nil, err
}

if !shouldPromptForFileSelection(cmd, files) {
return files, nil
}

selected, err := promptForAFFiles(files)
if err != nil {
return nil, err
}

if len(selected) == 0 {
return files, nil
}

return selected, nil
}

func shouldPromptForFileSelection(cmd *cobra.Command, files []string) bool {
if len(files) <= 1 {
return false
}
if cmd.Flags().Changed("dir") {
return false
}
return isInteractiveTTY()
}

func promptForAFFiles(files []string) ([]string, error) {
_, err := fmt.Fprintf(interactiveOutput, "Select .af files to generate (comma-separated numbers, blank or 'all' for all):\n")
if err != nil {
return nil, err
}

for i, file := range files {
if _, err := fmt.Fprintf(interactiveOutput, " %d. %s\n", i+1, file); err != nil {
return nil, err
}
}

if _, err := fmt.Fprint(interactiveOutput, "> "); err != nil {
return nil, err
}

selection, err := bufio.NewReader(interactiveInput).ReadString('\n')
if err != nil && err != io.EOF {
return nil, err
}

selection = strings.TrimSpace(selection)
if selection == "" || strings.EqualFold(selection, "all") {
return files, nil
}

chosen := make([]string, 0, len(files))
seen := make(map[int]struct{}, len(files))

for _, part := range strings.Split(selection, ",") {
part = strings.TrimSpace(part)
if part == "" {
continue
}

index, err := strconv.Atoi(part)
if err != nil {
return nil, fmt.Errorf("invalid selection %q: enter comma-separated numbers", part)
}
if index < 1 || index > len(files) {
return nil, fmt.Errorf("invalid selection %q: choose between 1 and %d", part, len(files))
}
if _, ok := seen[index]; ok {
continue
}
seen[index] = struct{}{}
chosen = append(chosen, files[index-1])
}

if len(chosen) == 0 {
return nil, fmt.Errorf("no files selected")
}

return chosen, nil
}
83 changes: 83 additions & 0 deletions cmd/af/gen/prompts/prompts_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package prompts

import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"

"github.com/omniaura/agentflow/pkg/assert/require"
"github.com/spf13/cobra"
)

func TestCollectAFFilesSkipsSymlinks(t *testing.T) {
Expand All @@ -23,3 +26,83 @@ func TestCollectAFFilesSkipsSymlinks(t *testing.T) {
t.Fatalf("expected 2 real .af files, got %d: %#v", len(files), files)
}
}

func TestShouldPromptForFileSelection(t *testing.T) {
cmd := &cobra.Command{Use: "prompts"}
cmd.Flags().String("dir", ".", "")

files := []string{"one.af", "two.af"}
originalTTY := isInteractiveTTY
isInteractiveTTY = func() bool { return true }
t.Cleanup(func() { isInteractiveTTY = originalTTY })

if !shouldPromptForFileSelection(cmd, files) {
t.Fatal("expected interactive prompt when dir flag is not set and tty is interactive")
}

require.NoError(t, cmd.Flags().Set("dir", "nested"))
if shouldPromptForFileSelection(cmd, files) {
t.Fatal("expected dir flag to disable interactive selection")
}
}

func TestPromptForAFFilesSelectsSubset(t *testing.T) {
originalInput := interactiveInput
originalOutput := interactiveOutput
interactiveInput = strings.NewReader("2, 1\n")
var out bytes.Buffer
interactiveOutput = &out
t.Cleanup(func() {
interactiveInput = originalInput
interactiveOutput = originalOutput
})

files := []string{"one.af", "two.af", "three.af"}
selected, err := promptForAFFiles(files)
require.NoError(t, err)

if len(selected) != 2 || selected[0] != "two.af" || selected[1] != "one.af" {
t.Fatalf("unexpected selection: %#v", selected)
}
if !strings.Contains(out.String(), "Select .af files to generate") {
t.Fatalf("expected prompt output, got %q", out.String())
}
}

func TestPromptForAFFilesSelectsAllOnBlankInput(t *testing.T) {
originalInput := interactiveInput
originalOutput := interactiveOutput
interactiveInput = strings.NewReader("\n")
interactiveOutput = &bytes.Buffer{}
t.Cleanup(func() {
interactiveInput = originalInput
interactiveOutput = originalOutput
})

files := []string{"one.af", "two.af"}
selected, err := promptForAFFiles(files)
require.NoError(t, err)

if len(selected) != len(files) {
t.Fatalf("expected all files, got %#v", selected)
}
}

func TestPromptForAFFilesRejectsInvalidSelection(t *testing.T) {
originalInput := interactiveInput
originalOutput := interactiveOutput
interactiveInput = strings.NewReader("4\n")
interactiveOutput = &bytes.Buffer{}
t.Cleanup(func() {
interactiveInput = originalInput
interactiveOutput = originalOutput
})

_, err := promptForAFFiles([]string{"one.af", "two.af"})
if err == nil {
t.Fatal("expected invalid selection error")
}
if !strings.Contains(err.Error(), "choose between 1 and 2") {
t.Fatalf("unexpected error: %v", err)
}
}
Loading