Merge pull request '[agent/codex:gpt-5.4-mini] Read ~/spec/code/core/go/cli/RFC.md fully. Find features des...' (#89) from agent/read---spec-code-core-go-cli-rfc-md-full into dev
All checks were successful
Security Scan / security (push) Successful in 23s
All checks were successful
Security Scan / security (push) Successful in 23s
This commit is contained in:
commit
e0aba4b863
7 changed files with 70 additions and 13 deletions
|
|
@ -8,6 +8,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Mode represents the CLI execution mode.
|
// Mode represents the CLI execution mode.
|
||||||
|
//
|
||||||
|
// mode := cli.DetectMode()
|
||||||
|
// if mode == cli.ModeDaemon {
|
||||||
|
// cli.LogInfo("running headless")
|
||||||
|
// }
|
||||||
type Mode int
|
type Mode int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -34,7 +39,11 @@ func (m Mode) String() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// DetectMode determines the execution mode based on environment.
|
// DetectMode determines the execution mode based on environment.
|
||||||
// Checks CORE_DAEMON env var first, then TTY status.
|
//
|
||||||
|
// mode := cli.DetectMode()
|
||||||
|
// // cli.ModeDaemon when CORE_DAEMON=1
|
||||||
|
// // cli.ModePipe when stdout is not a terminal
|
||||||
|
// // cli.ModeInteractive otherwise
|
||||||
func DetectMode() Mode {
|
func DetectMode() Mode {
|
||||||
if os.Getenv("CORE_DAEMON") == "1" {
|
if os.Getenv("CORE_DAEMON") == "1" {
|
||||||
return ModeDaemon
|
return ModeDaemon
|
||||||
|
|
@ -46,17 +55,28 @@ func DetectMode() Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTTY returns true if stdout is a terminal.
|
// IsTTY returns true if stdout is a terminal.
|
||||||
|
//
|
||||||
|
// if cli.IsTTY() {
|
||||||
|
// cli.Success("interactive output enabled")
|
||||||
|
// }
|
||||||
func IsTTY() bool {
|
func IsTTY() bool {
|
||||||
return term.IsTerminal(int(os.Stdout.Fd()))
|
return term.IsTerminal(int(os.Stdout.Fd()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsStdinTTY returns true if stdin is a terminal.
|
// IsStdinTTY returns true if stdin is a terminal.
|
||||||
|
//
|
||||||
|
// if !cli.IsStdinTTY() {
|
||||||
|
// cli.Warn("input is piped")
|
||||||
|
// }
|
||||||
func IsStdinTTY() bool {
|
func IsStdinTTY() bool {
|
||||||
return term.IsTerminal(int(os.Stdin.Fd()))
|
return term.IsTerminal(int(os.Stdin.Fd()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsStderrTTY returns true if stderr is a terminal.
|
// IsStderrTTY returns true if stderr is a terminal.
|
||||||
|
//
|
||||||
|
// if cli.IsStderrTTY() {
|
||||||
|
// cli.Progress("load", 1, 3, "config")
|
||||||
|
// }
|
||||||
func IsStderrTTY() bool {
|
func IsStderrTTY() bool {
|
||||||
return term.IsTerminal(int(os.Stderr.Fd()))
|
return term.IsTerminal(int(os.Stderr.Fd()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// DaemonOptions configures a background process helper.
|
// DaemonOptions configures a background process helper.
|
||||||
|
//
|
||||||
|
// daemon := cli.NewDaemon(cli.DaemonOptions{
|
||||||
|
// PIDFile: "/tmp/core.pid",
|
||||||
|
// HealthAddr: "127.0.0.1:8080",
|
||||||
|
// })
|
||||||
type DaemonOptions struct {
|
type DaemonOptions struct {
|
||||||
// PIDFile stores the current process ID on Start and removes it on Stop.
|
// PIDFile stores the current process ID on Start and removes it on Stop.
|
||||||
PIDFile string
|
PIDFile string
|
||||||
|
|
@ -41,6 +46,9 @@ type DaemonOptions struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Daemon manages a PID file and optional HTTP health endpoints.
|
// Daemon manages a PID file and optional HTTP health endpoints.
|
||||||
|
//
|
||||||
|
// daemon := cli.NewDaemon(cli.DaemonOptions{PIDFile: "/tmp/core.pid"})
|
||||||
|
// _ = daemon.Start(context.Background())
|
||||||
type Daemon struct {
|
type Daemon struct {
|
||||||
opts DaemonOptions
|
opts DaemonOptions
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,12 @@ func Join(errs ...error) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExitError represents an error that should cause the CLI to exit with a specific code.
|
// ExitError represents an error that should cause the CLI to exit with a specific code.
|
||||||
|
//
|
||||||
|
// err := cli.Exit(2, cli.Err("validation failed"))
|
||||||
|
// var exitErr *cli.ExitError
|
||||||
|
// if cli.As(err, &exitErr) {
|
||||||
|
// cli.Println("exit code:", exitErr.Code)
|
||||||
|
// }
|
||||||
type ExitError struct {
|
type ExitError struct {
|
||||||
Code int
|
Code int
|
||||||
Err error
|
Err error
|
||||||
|
|
@ -95,7 +101,8 @@ func (e *ExitError) Unwrap() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exit creates a new ExitError with the given code and error.
|
// Exit creates a new ExitError with the given code and error.
|
||||||
// Use this to return an error from a command with a specific exit code.
|
//
|
||||||
|
// return cli.Exit(2, cli.Err("validation failed"))
|
||||||
func Exit(code int, err error) error {
|
func Exit(code int, err error) error {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -76,14 +76,14 @@ func Select(label string, options []string) (string, error) {
|
||||||
input, err := r.ReadString('\n')
|
input, err := r.ReadString('\n')
|
||||||
if err != nil && strings.TrimSpace(input) == "" {
|
if err != nil && strings.TrimSpace(input) == "" {
|
||||||
promptHint("No input received. Selection cancelled.")
|
promptHint("No input received. Selection cancelled.")
|
||||||
return "", fmt.Errorf("selection cancelled: %w", err)
|
return "", Wrap(err, "selection cancelled")
|
||||||
}
|
}
|
||||||
|
|
||||||
trimmed := strings.TrimSpace(input)
|
trimmed := strings.TrimSpace(input)
|
||||||
n, err := strconv.Atoi(trimmed)
|
n, err := strconv.Atoi(trimmed)
|
||||||
if err != nil || n < 1 || n > len(options) {
|
if err != nil || n < 1 || n > len(options) {
|
||||||
promptHint(fmt.Sprintf("Please enter a number between 1 and %d.", len(options)))
|
promptHint(fmt.Sprintf("Please enter a number between 1 and %d.", len(options)))
|
||||||
return "", fmt.Errorf("invalid selection %q: choose a number between 1 and %d", trimmed, len(options))
|
return "", Err("invalid selection %q: choose a number between 1 and %d", trimmed, len(options))
|
||||||
}
|
}
|
||||||
return options[n-1], nil
|
return options[n-1], nil
|
||||||
}
|
}
|
||||||
|
|
@ -112,7 +112,7 @@ func MultiSelect(label string, options []string) ([]string, error) {
|
||||||
|
|
||||||
selected, parseErr := parseMultiSelection(trimmed, len(options))
|
selected, parseErr := parseMultiSelection(trimmed, len(options))
|
||||||
if parseErr != nil {
|
if parseErr != nil {
|
||||||
return nil, fmt.Errorf("invalid selection %q: %w", trimmed, parseErr)
|
return nil, Wrap(parseErr, fmt.Sprintf("invalid selection %q", trimmed))
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedOptions := make([]string, 0, len(selected))
|
selectedOptions := make([]string, 0, len(selected))
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// RenderStyle controls how layouts are rendered.
|
// RenderStyle controls how layouts are rendered.
|
||||||
|
//
|
||||||
|
// cli.UseRenderBoxed()
|
||||||
|
// frame := cli.NewFrame("HCF")
|
||||||
|
// fmt.Print(frame.String())
|
||||||
type RenderStyle int
|
type RenderStyle int
|
||||||
|
|
||||||
// Render style constants for layout output.
|
// Render style constants for layout output.
|
||||||
|
|
@ -21,12 +25,18 @@ const (
|
||||||
var currentRenderStyle = RenderFlat
|
var currentRenderStyle = RenderFlat
|
||||||
|
|
||||||
// UseRenderFlat sets the render style to flat (no borders).
|
// UseRenderFlat sets the render style to flat (no borders).
|
||||||
|
//
|
||||||
|
// cli.UseRenderFlat()
|
||||||
func UseRenderFlat() { currentRenderStyle = RenderFlat }
|
func UseRenderFlat() { currentRenderStyle = RenderFlat }
|
||||||
|
|
||||||
// UseRenderSimple sets the render style to simple (--- separators).
|
// UseRenderSimple sets the render style to simple (--- separators).
|
||||||
|
//
|
||||||
|
// cli.UseRenderSimple()
|
||||||
func UseRenderSimple() { currentRenderStyle = RenderSimple }
|
func UseRenderSimple() { currentRenderStyle = RenderSimple }
|
||||||
|
|
||||||
// UseRenderBoxed sets the render style to boxed (Unicode box drawing).
|
// UseRenderBoxed sets the render style to boxed (Unicode box drawing).
|
||||||
|
//
|
||||||
|
// cli.UseRenderBoxed()
|
||||||
func UseRenderBoxed() { currentRenderStyle = RenderBoxed }
|
func UseRenderBoxed() { currentRenderStyle = RenderBoxed }
|
||||||
|
|
||||||
// Render outputs the layout to terminal.
|
// Render outputs the layout to terminal.
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// StreamOption configures a Stream.
|
// StreamOption configures a Stream.
|
||||||
|
//
|
||||||
|
// stream := cli.NewStream(cli.WithWordWrap(80), cli.WithStreamOutput(os.Stdout))
|
||||||
type StreamOption func(*Stream)
|
type StreamOption func(*Stream)
|
||||||
|
|
||||||
// WithWordWrap sets the word-wrap column width.
|
// WithWordWrap sets the word-wrap column width.
|
||||||
|
|
@ -130,7 +132,7 @@ func (s *Stream) Column() int {
|
||||||
return s.col
|
return s.col
|
||||||
}
|
}
|
||||||
|
|
||||||
// Captured returns the stream output as a string when using a bytes.Buffer.
|
// Captured returns the stream output as a string when using a stringable writer.
|
||||||
// Panics if the output writer is not a *strings.Builder or fmt.Stringer.
|
// Panics if the output writer is not a *strings.Builder or fmt.Stringer.
|
||||||
func (s *Stream) Captured() string {
|
func (s *Stream) Captured() string {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,10 @@ func GhAuthenticated() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfirmOption configures Confirm behaviour.
|
// ConfirmOption configures Confirm behaviour.
|
||||||
|
//
|
||||||
|
// if cli.Confirm("Proceed?", cli.DefaultYes()) {
|
||||||
|
// cli.Success("continuing")
|
||||||
|
// }
|
||||||
type ConfirmOption func(*confirmConfig)
|
type ConfirmOption func(*confirmConfig)
|
||||||
|
|
||||||
type confirmConfig struct {
|
type confirmConfig struct {
|
||||||
|
|
@ -198,6 +202,8 @@ func ConfirmDangerousAction(verb, subject string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// QuestionOption configures Question behaviour.
|
// QuestionOption configures Question behaviour.
|
||||||
|
//
|
||||||
|
// name := cli.Question("Project name:", cli.WithDefault("my-app"))
|
||||||
type QuestionOption func(*questionConfig)
|
type QuestionOption func(*questionConfig)
|
||||||
|
|
||||||
type questionConfig struct {
|
type questionConfig struct {
|
||||||
|
|
@ -286,6 +292,10 @@ func QuestionAction(verb, subject string, opts ...QuestionOption) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChooseOption configures Choose behaviour.
|
// ChooseOption configures Choose behaviour.
|
||||||
|
//
|
||||||
|
// choice := cli.Choose("Pick one:", items, cli.Display(func(v Item) string {
|
||||||
|
// return v.Name
|
||||||
|
// }))
|
||||||
type ChooseOption[T any] func(*chooseConfig[T])
|
type ChooseOption[T any] func(*chooseConfig[T])
|
||||||
|
|
||||||
type chooseConfig[T any] struct {
|
type chooseConfig[T any] struct {
|
||||||
|
|
@ -579,17 +589,17 @@ func parseMultiSelection(input string, maxItems int) ([]int, error) {
|
||||||
rangeParts = append(rangeParts, p)
|
rangeParts = append(rangeParts, p)
|
||||||
}
|
}
|
||||||
if len(rangeParts) != 2 {
|
if len(rangeParts) != 2 {
|
||||||
return nil, fmt.Errorf("invalid range: %s", part)
|
return nil, Err("invalid range: %s", part)
|
||||||
}
|
}
|
||||||
var start, end int
|
var start, end int
|
||||||
if _, err := fmt.Sscanf(rangeParts[0], "%d", &start); err != nil {
|
if _, err := fmt.Sscanf(rangeParts[0], "%d", &start); err != nil {
|
||||||
return nil, fmt.Errorf("invalid range start: %s", rangeParts[0])
|
return nil, Err("invalid range start: %s", rangeParts[0])
|
||||||
}
|
}
|
||||||
if _, err := fmt.Sscanf(rangeParts[1], "%d", &end); err != nil {
|
if _, err := fmt.Sscanf(rangeParts[1], "%d", &end); err != nil {
|
||||||
return nil, fmt.Errorf("invalid range end: %s", rangeParts[1])
|
return nil, Err("invalid range end: %s", rangeParts[1])
|
||||||
}
|
}
|
||||||
if start < 1 || start > maxItems || end < 1 || end > maxItems || start > end {
|
if start < 1 || start > maxItems || end < 1 || end > maxItems || start > end {
|
||||||
return nil, fmt.Errorf("range out of bounds: %s", part)
|
return nil, Err("range out of bounds: %s", part)
|
||||||
}
|
}
|
||||||
for i := start; i <= end; i++ {
|
for i := start; i <= end; i++ {
|
||||||
selected[i-1] = true // Convert to 0-based
|
selected[i-1] = true // Convert to 0-based
|
||||||
|
|
@ -598,10 +608,10 @@ func parseMultiSelection(input string, maxItems int) ([]int, error) {
|
||||||
// Single number
|
// Single number
|
||||||
var n int
|
var n int
|
||||||
if _, err := fmt.Sscanf(part, "%d", &n); err != nil {
|
if _, err := fmt.Sscanf(part, "%d", &n); err != nil {
|
||||||
return nil, fmt.Errorf("invalid number: %s", part)
|
return nil, Err("invalid number: %s", part)
|
||||||
}
|
}
|
||||||
if n < 1 || n > maxItems {
|
if n < 1 || n > maxItems {
|
||||||
return nil, fmt.Errorf("number out of range: %d", n)
|
return nil, Err("number out of range: %d", n)
|
||||||
}
|
}
|
||||||
selected[n-1] = true // Convert to 0-based
|
selected[n-1] = true // Convert to 0-based
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue