diff --git a/pkg/cli/daemon.go b/pkg/cli/daemon.go index 6fb6c06..d116848 100644 --- a/pkg/cli/daemon.go +++ b/pkg/cli/daemon.go @@ -8,6 +8,11 @@ import ( ) // Mode represents the CLI execution mode. +// +// mode := cli.DetectMode() +// if mode == cli.ModeDaemon { +// cli.LogInfo("running headless") +// } type Mode int const ( @@ -34,7 +39,11 @@ func (m Mode) String() string { } // 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 { if os.Getenv("CORE_DAEMON") == "1" { return ModeDaemon @@ -46,17 +55,28 @@ func DetectMode() Mode { } // IsTTY returns true if stdout is a terminal. +// +// if cli.IsTTY() { +// cli.Success("interactive output enabled") +// } func IsTTY() bool { return term.IsTerminal(int(os.Stdout.Fd())) } // IsStdinTTY returns true if stdin is a terminal. +// +// if !cli.IsStdinTTY() { +// cli.Warn("input is piped") +// } func IsStdinTTY() bool { return term.IsTerminal(int(os.Stdin.Fd())) } // IsStderrTTY returns true if stderr is a terminal. +// +// if cli.IsStderrTTY() { +// cli.Progress("load", 1, 3, "config") +// } func IsStderrTTY() bool { return term.IsTerminal(int(os.Stderr.Fd())) } - diff --git a/pkg/cli/daemon_process.go b/pkg/cli/daemon_process.go index f4d8aed..a724012 100644 --- a/pkg/cli/daemon_process.go +++ b/pkg/cli/daemon_process.go @@ -17,6 +17,11 @@ import ( ) // DaemonOptions configures a background process helper. +// +// daemon := cli.NewDaemon(cli.DaemonOptions{ +// PIDFile: "/tmp/core.pid", +// HealthAddr: "127.0.0.1:8080", +// }) type DaemonOptions struct { // PIDFile stores the current process ID on Start and removes it on Stop. PIDFile string @@ -41,6 +46,9 @@ type DaemonOptions struct { } // 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 { opts DaemonOptions diff --git a/pkg/cli/errors.go b/pkg/cli/errors.go index f3fc105..0c3cd1f 100644 --- a/pkg/cli/errors.go +++ b/pkg/cli/errors.go @@ -78,6 +78,12 @@ func Join(errs ...error) error { } // 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 { Code int Err error @@ -95,7 +101,8 @@ func (e *ExitError) Unwrap() 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 { if err == nil { return nil diff --git a/pkg/cli/prompt.go b/pkg/cli/prompt.go index 3197227..bc90caf 100644 --- a/pkg/cli/prompt.go +++ b/pkg/cli/prompt.go @@ -76,14 +76,14 @@ func Select(label string, options []string) (string, error) { input, err := r.ReadString('\n') if err != nil && strings.TrimSpace(input) == "" { promptHint("No input received. Selection cancelled.") - return "", fmt.Errorf("selection cancelled: %w", err) + return "", Wrap(err, "selection cancelled") } trimmed := strings.TrimSpace(input) n, err := strconv.Atoi(trimmed) if err != nil || n < 1 || n > 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 } @@ -112,7 +112,7 @@ func MultiSelect(label string, options []string) ([]string, error) { selected, parseErr := parseMultiSelection(trimmed, len(options)) 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)) diff --git a/pkg/cli/render.go b/pkg/cli/render.go index baab337..7075d89 100644 --- a/pkg/cli/render.go +++ b/pkg/cli/render.go @@ -6,6 +6,10 @@ import ( ) // RenderStyle controls how layouts are rendered. +// +// cli.UseRenderBoxed() +// frame := cli.NewFrame("HCF") +// fmt.Print(frame.String()) type RenderStyle int // Render style constants for layout output. @@ -21,12 +25,18 @@ const ( var currentRenderStyle = RenderFlat // UseRenderFlat sets the render style to flat (no borders). +// +// cli.UseRenderFlat() func UseRenderFlat() { currentRenderStyle = RenderFlat } // UseRenderSimple sets the render style to simple (--- separators). +// +// cli.UseRenderSimple() func UseRenderSimple() { currentRenderStyle = RenderSimple } // UseRenderBoxed sets the render style to boxed (Unicode box drawing). +// +// cli.UseRenderBoxed() func UseRenderBoxed() { currentRenderStyle = RenderBoxed } // Render outputs the layout to terminal. diff --git a/pkg/cli/stream.go b/pkg/cli/stream.go index 0e4e8d0..211fd6d 100644 --- a/pkg/cli/stream.go +++ b/pkg/cli/stream.go @@ -11,6 +11,8 @@ import ( ) // StreamOption configures a Stream. +// +// stream := cli.NewStream(cli.WithWordWrap(80), cli.WithStreamOutput(os.Stdout)) type StreamOption func(*Stream) // WithWordWrap sets the word-wrap column width. @@ -130,7 +132,7 @@ func (s *Stream) Column() int { 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. func (s *Stream) Captured() string { s.mu.Lock() diff --git a/pkg/cli/utils.go b/pkg/cli/utils.go index ecab93f..e350612 100644 --- a/pkg/cli/utils.go +++ b/pkg/cli/utils.go @@ -31,6 +31,10 @@ func GhAuthenticated() bool { } // ConfirmOption configures Confirm behaviour. +// +// if cli.Confirm("Proceed?", cli.DefaultYes()) { +// cli.Success("continuing") +// } type ConfirmOption func(*confirmConfig) type confirmConfig struct { @@ -198,6 +202,8 @@ func ConfirmDangerousAction(verb, subject string) bool { } // QuestionOption configures Question behaviour. +// +// name := cli.Question("Project name:", cli.WithDefault("my-app")) type QuestionOption func(*questionConfig) type questionConfig struct { @@ -286,6 +292,10 @@ func QuestionAction(verb, subject string, opts ...QuestionOption) string { } // 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 chooseConfig[T any] struct { @@ -579,17 +589,17 @@ func parseMultiSelection(input string, maxItems int) ([]int, error) { rangeParts = append(rangeParts, p) } if len(rangeParts) != 2 { - return nil, fmt.Errorf("invalid range: %s", part) + return nil, Err("invalid range: %s", part) } var start, end int 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 { - 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 { - 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++ { selected[i-1] = true // Convert to 0-based @@ -598,10 +608,10 @@ func parseMultiSelection(input string, maxItems int) ([]int, error) { // Single number var n int 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 { - 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 }