ajhahn.de
← eeco
Go 80 lines
package projecttype

// prompter.go is the Layer-3 interactive selection: when the deterministic
// scan is ambiguous and no --type/--ai flag forced the outcome, eeco asks the
// operator to pick a category, describe the project (routing to Layer 4), or
// fall back to generic. It is AI-free and reads a single readline.

import (
	"bufio"
	"fmt"
	"io"
	"strconv"
	"strings"
)

// stdinPrompter is the deterministic, AI-free layer-3 prompter: a single
// readline question with numbered candidates and two escapes — describe
// the project freely (routes to the AI layer) or pick the generic
// fallback.
type stdinPrompter struct {
	in  *bufio.Scanner
	out io.Writer
}

// NewStdinPrompter builds a Prompter that reads operator choices from in
// and writes its question to out. It is the prompter `eeco init` wires
// to os.Stdin / os.Stderr; tests inject buffers.
func NewStdinPrompter(in io.Reader, out io.Writer) Prompter {
	return &stdinPrompter{in: bufio.NewScanner(in), out: out}
}

const maxPromptRetries = 5

func (p *stdinPrompter) Pick(candidates []Category, cat *Catalog) (Category, bool, string, error) {
	fmt.Fprintln(p.out, "eeco init: project type is ambiguous.")
	for i, c := range candidates {
		desc := ""
		if e, ok := cat.Get(c); ok {
			desc = e.Description
		}
		fmt.Fprintf(p.out, "  %d) %s%s\n", i+1, c, desc)
	}
	fmt.Fprintln(p.out, "  d) describe the project in your own words")
	fmt.Fprintln(p.out, "  g) generic (no project-specific scaffold)")

	for range maxPromptRetries {
		if len(candidates) > 0 {
			fmt.Fprintf(p.out, "pick [1-%d/d/g]: ", len(candidates))
		} else {
			fmt.Fprint(p.out, "pick [d/g]: ")
		}
		if !p.in.Scan() {
			// No more input (EOF / closed stdin): degrade to generic so a
			// piped, non-interactive `eeco init` still completes.
			return Generic, false, "", nil
		}
		line := strings.TrimSpace(p.in.Text())
		switch strings.ToLower(line) {
		case "d":
			fmt.Fprint(p.out, "describe the project: ")
			free := ""
			if p.in.Scan() {
				free = strings.TrimSpace(p.in.Text())
			}
			return "", true, free, nil
		case "g", "":
			if line == "" {
				continue
			}
			return Generic, false, "", nil
		}
		n, err := strconv.Atoi(line)
		if err == nil && n >= 1 && n <= len(candidates) {
			return candidates[n-1], false, "", nil
		}
		fmt.Fprintln(p.out, "  unrecognised choice; try again.")
	}
	return Generic, false, "", nil
}