Merge pull request '[agent/codex:gpt-5.4-mini] Read ~/spec/code/core/go/cli/RFC.md fully. Find features des...' (#79) from agent/read---spec-code-core-go-cli-rfc-md-full into dev
All checks were successful
Security Scan / security (push) Successful in 21s

This commit is contained in:
Virgil 2026-04-02 11:17:17 +00:00
commit 32e096c6e1
5 changed files with 80 additions and 23 deletions

View file

@ -114,15 +114,15 @@ func Dim(msg string) {
func Progress(verb string, current, total int, item ...string) { func Progress(verb string, current, total int, item ...string) {
msg := i18n.Progress(verb) msg := i18n.Progress(verb)
if len(item) > 0 && item[0] != "" { if len(item) > 0 && item[0] != "" {
fmt.Printf("\033[2K\r%s %d/%d %s", DimStyle.Render(msg), current, total, item[0]) fmt.Fprintf(os.Stderr, "\033[2K\r%s %d/%d %s", DimStyle.Render(msg), current, total, item[0])
} else { } else {
fmt.Printf("\033[2K\r%s %d/%d", DimStyle.Render(msg), current, total) fmt.Fprintf(os.Stderr, "\033[2K\r%s %d/%d", DimStyle.Render(msg), current, total)
} }
} }
// ProgressDone clears the progress line. // ProgressDone clears the progress line.
func ProgressDone() { func ProgressDone() {
fmt.Print("\033[2K\r") fmt.Fprint(os.Stderr, "\033[2K\r")
} }
// Label prints a "Label: value" line. // Label prints a "Label: value" line.

View file

@ -35,9 +35,9 @@ func Prompt(label, defaultVal string) (string, error) {
label = compileGlyphs(label) label = compileGlyphs(label)
defaultVal = compileGlyphs(defaultVal) defaultVal = compileGlyphs(defaultVal)
if defaultVal != "" { if defaultVal != "" {
fmt.Printf("%s [%s]: ", label, defaultVal) fmt.Fprintf(os.Stderr, "%s [%s]: ", label, defaultVal)
} else { } else {
fmt.Printf("%s: ", label) fmt.Fprintf(os.Stderr, "%s: ", label)
} }
r := newReader() r := newReader()
@ -66,11 +66,11 @@ func Select(label string, options []string) (string, error) {
return "", nil return "", nil
} }
fmt.Println(compileGlyphs(label)) fmt.Fprintln(os.Stderr, compileGlyphs(label))
for i, opt := range options { for i, opt := range options {
fmt.Printf(" %d. %s\n", i+1, compileGlyphs(opt)) fmt.Fprintf(os.Stderr, " %d. %s\n", i+1, compileGlyphs(opt))
} }
fmt.Printf("Choose [1-%d]: ", len(options)) fmt.Fprintf(os.Stderr, "Choose [1-%d]: ", len(options))
r := newReader() r := newReader()
input, err := r.ReadString('\n') input, err := r.ReadString('\n')
@ -94,11 +94,11 @@ func MultiSelect(label string, options []string) ([]string, error) {
return []string{}, nil return []string{}, nil
} }
fmt.Println(compileGlyphs(label)) fmt.Fprintln(os.Stderr, compileGlyphs(label))
for i, opt := range options { for i, opt := range options {
fmt.Printf(" %d. %s\n", i+1, compileGlyphs(opt)) fmt.Fprintf(os.Stderr, " %d. %s\n", i+1, compileGlyphs(opt))
} }
fmt.Printf("Choose (space-separated) [1-%d]: ", len(options)) fmt.Fprintf(os.Stderr, "Choose (space-separated) [1-%d]: ", len(options))
r := newReader() r := newReader()
input, err := r.ReadString('\n') input, err := r.ReadString('\n')

View file

@ -38,6 +38,49 @@ func captureStderr(t *testing.T, fn func()) string {
return buf.String() return buf.String()
} }
func captureStdoutStderr(t *testing.T, fn func()) (string, string) {
t.Helper()
oldOut := os.Stdout
oldErr := os.Stderr
rOut, wOut, err := os.Pipe()
if !assert.NoError(t, err) {
return "", ""
}
rErr, wErr, err := os.Pipe()
if !assert.NoError(t, err) {
return "", ""
}
os.Stdout = wOut
os.Stderr = wErr
defer func() {
os.Stdout = oldOut
os.Stderr = oldErr
}()
fn()
if !assert.NoError(t, wOut.Close()) {
return "", ""
}
if !assert.NoError(t, wErr.Close()) {
return "", ""
}
var outBuf bytes.Buffer
var errBuf bytes.Buffer
_, err = io.Copy(&outBuf, rOut)
if !assert.NoError(t, err) {
return "", ""
}
_, err = io.Copy(&errBuf, rErr)
if !assert.NoError(t, err) {
return "", ""
}
return outBuf.String(), errBuf.String()
}
func TestPrompt_Good(t *testing.T) { func TestPrompt_Good(t *testing.T) {
SetStdin(strings.NewReader("hello\n")) SetStdin(strings.NewReader("hello\n"))
defer SetStdin(nil) // reset defer SetStdin(nil) // reset
@ -353,3 +396,17 @@ func TestPromptHints_Good_UseStderr(t *testing.T) {
assert.Contains(t, stderr.String(), "try again") assert.Contains(t, stderr.String(), "try again")
assert.Contains(t, stderr.String(), "invalid") assert.Contains(t, stderr.String(), "invalid")
} }
func TestPrompt_Good_WritesToStderr(t *testing.T) {
SetStdin(strings.NewReader("hello\n"))
defer SetStdin(nil)
stdout, stderr := captureStdoutStderr(t, func() {
val, err := Prompt("Name", "")
assert.NoError(t, err)
assert.Equal(t, "hello", val)
})
assert.Empty(t, stdout)
assert.Contains(t, stderr, "Name:")
}

View file

@ -114,7 +114,7 @@ func (tr *TaskTracker) Snapshots() iter.Seq2[string, string] {
// NewTaskTracker creates a new parallel task tracker. // NewTaskTracker creates a new parallel task tracker.
func NewTaskTracker() *TaskTracker { func NewTaskTracker() *TaskTracker {
return &TaskTracker{out: os.Stdout} return &TaskTracker{out: os.Stderr}
} }
// Add registers a task and returns it for goroutine use. // Add registers a task and returns it for goroutine use.

View file

@ -110,7 +110,7 @@ func Confirm(prompt string, opts ...ConfirmOption) bool {
reader := newReader() reader := newReader()
for { for {
fmt.Printf("%s %s", prompt, suffix) fmt.Fprintf(os.Stderr, "%s %s", prompt, suffix)
var response string var response string
var readErr error var readErr error
@ -130,7 +130,7 @@ func Confirm(prompt string, opts ...ConfirmOption) bool {
readErr = <-errChan readErr = <-errChan
response = strings.ToLower(strings.TrimSpace(response)) response = strings.ToLower(strings.TrimSpace(response))
case <-time.After(cfg.timeout): case <-time.After(cfg.timeout):
fmt.Println() // New line after timeout fmt.Fprintln(os.Stderr) // New line after timeout
return cfg.defaultYes return cfg.defaultYes
} }
} else { } else {
@ -245,9 +245,9 @@ func Question(prompt string, opts ...QuestionOption) string {
for { for {
// Build prompt with default // Build prompt with default
if cfg.defaultValue != "" { if cfg.defaultValue != "" {
fmt.Printf("%s [%s] ", prompt, compileGlyphs(cfg.defaultValue)) fmt.Fprintf(os.Stderr, "%s [%s] ", prompt, compileGlyphs(cfg.defaultValue))
} else { } else {
fmt.Printf("%s ", prompt) fmt.Fprintf(os.Stderr, "%s ", prompt)
} }
response, err := reader.ReadString('\n') response, err := reader.ReadString('\n')
@ -364,9 +364,9 @@ func Choose[T any](prompt string, items []T, opts ...ChooseOption[T]) T {
renderChoices(prompt, items, visible, cfg.displayFn, cfg.defaultN, cfg.filter) renderChoices(prompt, items, visible, cfg.displayFn, cfg.defaultN, cfg.filter)
if cfg.filter { if cfg.filter {
fmt.Printf("Enter number [1-%d] or filter: ", len(visible)) fmt.Fprintf(os.Stderr, "Enter number [1-%d] or filter: ", len(visible))
} else { } else {
fmt.Printf("Enter number [1-%d]: ", len(visible)) fmt.Fprintf(os.Stderr, "Enter number [1-%d]: ", len(visible))
} }
response, err := reader.ReadString('\n') response, err := reader.ReadString('\n')
response = strings.TrimSpace(response) response = strings.TrimSpace(response)
@ -458,9 +458,9 @@ func ChooseMulti[T any](prompt string, items []T, opts ...ChooseOption[T]) []T {
renderChoices(prompt, items, visible, cfg.displayFn, -1, cfg.filter) renderChoices(prompt, items, visible, cfg.displayFn, -1, cfg.filter)
if cfg.filter { if cfg.filter {
fmt.Printf("Enter numbers (e.g., 1 3 5 or 1-3), or filter text, or empty for none: ") fmt.Fprint(os.Stderr, "Enter numbers (e.g., 1 3 5 or 1-3), or filter text, or empty for none: ")
} else { } else {
fmt.Printf("Enter numbers (e.g., 1 3 5 or 1-3) or empty for none: ") fmt.Fprint(os.Stderr, "Enter numbers (e.g., 1 3 5 or 1-3) or empty for none: ")
} }
response, _ := reader.ReadString('\n') response, _ := reader.ReadString('\n')
response = strings.TrimSpace(response) response = strings.TrimSpace(response)
@ -503,16 +503,16 @@ func ChooseMulti[T any](prompt string, items []T, opts ...ChooseOption[T]) []T {
} }
func renderChoices[T any](prompt string, items []T, visible []int, displayFn func(T) string, defaultN int, filter bool) { func renderChoices[T any](prompt string, items []T, visible []int, displayFn func(T) string, defaultN int, filter bool) {
fmt.Println(prompt) fmt.Fprintln(os.Stderr, prompt)
for i, idx := range visible { for i, idx := range visible {
marker := " " marker := " "
if defaultN >= 0 && idx == defaultN { if defaultN >= 0 && idx == defaultN {
marker = "*" marker = "*"
} }
fmt.Printf(" %s%d. %s\n", marker, i+1, compileGlyphs(displayFn(items[idx]))) fmt.Fprintf(os.Stderr, " %s%d. %s\n", marker, i+1, compileGlyphs(displayFn(items[idx])))
} }
if filter { if filter {
fmt.Println(" (type to filter the list)") fmt.Fprintln(os.Stderr, " (type to filter the list)")
} }
} }