Documentation
¶
Overview ¶
Package interactive provides infrastructure adapter for interactive prompts.
Index ¶
- Variables
- func ConfirmLocalBinarySelection(config *SelectionConfig) (bool, error)
- func ConfirmReplaceSelection(version string) (bool, error)
- func ConfirmSelection(config *SelectionConfig) (bool, error)
- func ConfirmUpgradeSelection(config *UpgradeSelectionConfig, skipUpgradeName bool) (bool, error)
- func IsCancellation(err error) bool
- func IsTerminalInteractive() bool
- func PromptUpgradeName(defaultName string) (string, error)
- func SelectDockerImage(versions []github.ImageVersion) (string, bool, error)
- func SelectNetwork() (string, error)
- func SelectVersion(label string, releases []github.GitHubRelease, defaultVersion string) (string, bool, error)
- type Adapter
- func (a *Adapter) ConfirmAction(message string) (bool, error)
- func (a *Adapter) ConfirmSelection(config *ports.SelectionConfig) (bool, error)
- func (a *Adapter) ConfirmUpgradeSelection(config *ports.UpgradeSelectionConfig) (bool, error)
- func (a *Adapter) PromptUpgradeName(defaultName string) (string, error)
- func (a *Adapter) SelectDockerImage(prompt string, versions []ports.ImageVersion, defaultVersion string) (string, bool, error)
- func (a *Adapter) SelectNetwork() (string, error)
- func (a *Adapter) SelectVersion(prompt string, releases []ports.GitHubRelease, defaultVersion string) (string, bool, error)
- type BinarySelectionOptions
- type BinarySelectionResult
- type BinarySelector
- type CancellationError
- type DockerImageItem
- type FilesystemBrowserAdapter
- type NetworkOption
- type PathCompleterAdapter
- type Prompter
- type PrompterAdapter
- type SelectionConfig
- type Selector
- func (s *Selector) RunSelectionFlow(ctx context.Context) (*SelectionConfig, error)
- func (s *Selector) RunUpgradeSelectionFlow(ctx context.Context, skipUpgradeName bool) (*UpgradeSelectionConfig, error)
- func (s *Selector) RunVersionSelectionFlow(ctx context.Context, network string) (*SelectionConfig, error)
- type SourceSelectorAdapter
- type UpgradeSelectionConfig
- type VersionItem
Constants ¶
This section is empty.
Variables ¶
var Networks = []NetworkOption{
{Name: "mainnet", Description: "Stable mainnet network"},
{Name: "testnet", Description: "Stable testnet network"},
}
Networks available for selection.
Functions ¶
func ConfirmLocalBinarySelection ¶
func ConfirmLocalBinarySelection(config *SelectionConfig) (bool, error)
ConfirmLocalBinarySelection prompts the user to confirm their local binary selection.
func ConfirmReplaceSelection ¶
ConfirmReplaceSelection prompts the user to confirm their replace selection.
func ConfirmSelection ¶
func ConfirmSelection(config *SelectionConfig) (bool, error)
ConfirmSelection prompts the user to confirm their selection for start command.
func ConfirmUpgradeSelection ¶
func ConfirmUpgradeSelection(config *UpgradeSelectionConfig, skipUpgradeName bool) (bool, error)
ConfirmUpgradeSelection prompts the user to confirm their upgrade selection. When skipUpgradeName is true (e.g., --skip-gov mode), it skips displaying the upgrade name.
func IsCancellation ¶
IsCancellation returns true if the error is a cancellation error.
func IsTerminalInteractive ¶
func IsTerminalInteractive() bool
IsTerminalInteractive checks if the current environment supports interactive prompts.
This implements EC-004 detection logic using golang.org/x/term package.
Returns:
- true if stdout is a TTY (terminal) - interactive mode
- false if stdout is piped/redirected - non-interactive mode (CI/CD)
Examples:
- Interactive: `./devnet-builder deploy --mode local`
- Non-interactive: `echo "" | ./devnet-builder deploy --mode local`
- Non-interactive: Running in CI/CD pipeline
Note: This is a helper function that can be called from deploy.go/upgrade.go before calling RunBinarySelectionFlow to set opts.IsInteractive.
func PromptUpgradeName ¶
PromptUpgradeName prompts the user to enter an upgrade handler name.
func SelectDockerImage ¶
func SelectDockerImage(versions []github.ImageVersion) (string, bool, error)
SelectDockerImage prompts the user to select a docker image version from GHCR. Users can also enter a custom image URL. Returns the selected image tag (or full URL for custom), and whether it's a custom image.
func SelectNetwork ¶
SelectNetwork prompts the user to select a network.
func SelectVersion ¶
func SelectVersion(label string, releases []github.GitHubRelease, defaultVersion string) (string, bool, error)
SelectVersion prompts the user to select a version from the list. Users can also enter a custom branch name or commit hash. Returns the selected version and whether it's a custom ref.
Types ¶
type Adapter ¶
type Adapter struct{}
Adapter implements ports.InteractiveSelector using promptui.
func (*Adapter) ConfirmAction ¶
ConfirmAction asks user to confirm a generic action.
func (*Adapter) ConfirmSelection ¶
func (a *Adapter) ConfirmSelection(config *ports.SelectionConfig) (bool, error)
ConfirmSelection asks user to confirm their selection.
func (*Adapter) ConfirmUpgradeSelection ¶
func (a *Adapter) ConfirmUpgradeSelection(config *ports.UpgradeSelectionConfig) (bool, error)
ConfirmUpgradeSelection asks user to confirm upgrade selection.
func (*Adapter) PromptUpgradeName ¶
PromptUpgradeName prompts user for an upgrade name.
func (*Adapter) SelectDockerImage ¶
func (a *Adapter) SelectDockerImage(prompt string, versions []ports.ImageVersion, defaultVersion string) (string, bool, error)
SelectDockerImage prompts user to select a docker image version.
func (*Adapter) SelectNetwork ¶
SelectNetwork prompts user to select a network.
func (*Adapter) SelectVersion ¶
func (a *Adapter) SelectVersion(prompt string, releases []ports.GitHubRelease, defaultVersion string) (string, bool, error)
SelectVersion prompts user to select a version from releases.
type BinarySelectionOptions ¶
type BinarySelectionOptions struct {
// AllowBuildFromSource adds "Build from source" option to the list
// Set to true for deploy command, false for selection-only scenarios
AllowBuildFromSource bool
// AutoSelectSingle automatically selects when only one valid binary exists
// Per CLARIFICATION 1: Option A - Auto-select silently with info log
AutoSelectSingle bool
// IsInteractive indicates if running in TTY (interactive terminal)
// Per CLARIFICATION 2: Non-TTY environments auto-select first valid binary
IsInteractive bool
}
BinarySelectionOptions configures the binary selection behavior. This struct encapsulates all configuration needed for different selection scenarios.
type BinarySelectionResult ¶
type BinarySelectionResult struct {
// SelectedBinary points to the chosen binary metadata (nil if build selected)
SelectedBinary *cache.CachedBinaryMetadata
// BinaryPath is the absolute path to the selected binary
BinaryPath string
// ShouldBuild is true if user selected "Build from source"
ShouldBuild bool
// BuildVersion is the version/ref to build (only if ShouldBuild=true)
BuildVersion string
// WasCancelled is true if user pressed Ctrl+C or ESC
WasCancelled bool
}
BinarySelectionResult represents the outcome of binary selection. This follows the Result pattern for explicit success/failure handling.
type BinarySelector ¶
type BinarySelector struct {
// contains filtered or unexported fields
}
BinarySelector handles interactive binary selection with all edge cases. This implements the Selector component from the implementation plan (Task T032-T033).
Responsibilities:
- Display formatted binary list with arrow key navigation
- Handle all edge cases defined in spec (EC-001 to EC-012)
- Support both interactive and non-interactive modes
- Integrate with promptui for consistent UX
Design Decision: Separate selector from scanner/validator for Single Responsibility. Scanner finds binaries, Validator checks them, Selector presents choices.
func NewBinarySelector ¶
func NewBinarySelector(prompter Prompter) *BinarySelector
NewBinarySelector creates a new binary selector with the given prompter.
Parameters:
- prompter: Prompt adapter (use NewPrompterAdapter() for production, mock for tests)
Example:
selector := NewBinarySelector(NewPrompterAdapter()) result, err := selector.RunBinarySelectionFlow(ctx, binaries, opts)
func (*BinarySelector) RunBinarySelectionFlow ¶
func (s *BinarySelector) RunBinarySelectionFlow( ctx context.Context, binaries []cache.CachedBinaryMetadata, opts BinarySelectionOptions, ) (*BinarySelectionResult, error)
RunBinarySelectionFlow orchestrates the complete binary selection process.
This method implements all functional requirements and edge cases from the spec:
- FR-003: Interactive binary list display with formatting
- FR-005: Binary resolution priority order
- FR-010: Non-interactive mode handling
- FR-011: Build from source option
- EC-001 to EC-012: All edge case scenarios
Edge Cases Handled:
- EC-001: Zero binaries → returns empty result (caller decides to build)
- EC-002: Single binary + auto-select → returns immediately with info log
- EC-004: Non-TTY environment → auto-selects first valid binary
- EC-005: User cancellation → returns WasCancelled=true
- EC-008: Large cache (>50 binaries) → all displayed with scrolling
Parameters:
- ctx: Context for cancellation (currently unused but reserved)
- binaries: Valid binaries from scanner (pre-filtered by validator)
- opts: Selection options (auto-select, interactive mode, build option)
Returns:
- BinarySelectionResult with selection outcome
- Error only for unexpected failures (not for cancellation)
User Interaction Flow:
- Check for edge cases (zero, single, non-TTY)
- Display formatted list with arrow key navigation
- User selects binary or "Build from source"
- If build selected, prompt for version/ref
- Return result with selected binary or build request
type CancellationError ¶
type CancellationError struct {
Message string
}
CancellationError indicates the user cancelled the operation.
func (*CancellationError) Error ¶
func (e *CancellationError) Error() string
type DockerImageItem ¶
type DockerImageItem struct {
Tag string
CreatedAt time.Time
IsLatest bool
IsCustom bool // True for "Enter custom image..." option
}
DockerImageItem represents a docker image version for display in promptui.
func (DockerImageItem) String ¶
func (d DockerImageItem) String() string
String returns display string for promptui.
type FilesystemBrowserAdapter ¶
type FilesystemBrowserAdapter struct {
// contains filtered or unexported fields
}
FilesystemBrowserAdapter implements the FilesystemBrowser port interface using readline for interactive path selection with tab auto-completion.
This adapter follows Clean Architecture principles by:
- Implementing the port interface defined in the application layer
- Using infrastructure-specific code (readline) isolated to this layer
Design Decisions:
- Uses chzyer/readline for simple tab completion
- Leverages existing PathCompleter for file system navigation
- Tab key provides auto-completion suggestions
- Simple and lightweight implementation
- Validates selected path using os.Stat and executable permission checks
User Experience:
- Type path and press Tab for auto-completion
- Type `/`: shows root directory contents
- Type `./`: shows current directory contents
- Type `~/`: shows home directory contents
- Arrow keys: navigate input history
- Enter: select and validate path
- Ctrl+C: cancel selection
Performance:
- Efficient prefix matching
- On-demand directory scanning
func NewFilesystemBrowserAdapter ¶
func NewFilesystemBrowserAdapter(pathCompleter ports.PathCompleter) *FilesystemBrowserAdapter
NewFilesystemBrowserAdapter creates a new FilesystemBrowserAdapter instance.
func (*FilesystemBrowserAdapter) BrowsePath ¶
func (f *FilesystemBrowserAdapter) BrowsePath(ctx context.Context, initialPath string) (string, error)
BrowsePath prompts the user to browse and select a filesystem path using an interactive prompt.
This method implements the interactive file browsing flow using readline:
- Display interactive input with auto-completion
- User types path with tab completion
- User presses Tab to see auto-completion suggestions
- User presses Enter to select a file
- Validate selected path: - File exists (not directory) - File is executable (mode & 0111 != 0) - Symlinks are resolved (max depth 10)
- If validation fails, show error and allow retry
- If validation succeeds, return absolute path
Parameters:
- ctx: Context for cancellation and timeout control
- initialPath: Starting directory hint
Returns:
- string: Absolute path to the selected binary file
- error: Validation error, cancellation error, or system error
type NetworkOption ¶
NetworkOption represents a network option for selection.
type PathCompleterAdapter ¶
type PathCompleterAdapter struct{}
PathCompleterAdapter implements the PathCompleter port interface using the standard library's os and filepath packages for filesystem operations.
This adapter follows Clean Architecture principles by implementing the port interface defined in the application layer (ports.PathCompleter) using infrastructure-specific code (os.ReadDir, filepath operations).
Design Decisions:
- Uses os.ReadDir for directory listing (faster than filepath.Walk for single directory)
- Implements silent failure for permission errors and non-existent directories
- Adds trailing "/" to directories for visual distinction in autocomplete
- Limits results to 100 entries for performance in large directories
- Sorts alphabetically for consistent user experience
Performance:
- Target: < 100ms for directories with < 1000 entries (SC-002)
- os.ReadDir is optimized for this use case (no stat() calls during iteration)
- Early termination at 100 results prevents slowdown in large directories
func NewPathCompleterAdapter ¶
func NewPathCompleterAdapter() *PathCompleterAdapter
NewPathCompleterAdapter creates a new PathCompleterAdapter instance.
This is a simple constructor with no dependencies, making it easy to use in various contexts (interactive CLI, tests, etc.).
Returns:
- *PathCompleterAdapter: A new instance ready for use
func (*PathCompleterAdapter) Complete ¶
func (p *PathCompleterAdapter) Complete(input string) []string
Complete generates autocomplete suggestions for the given input path.
This method implements the core autocomplete logic:
- Parse input into directory + partial filename
- Handle special cases (empty input, root directory)
- List directory contents using os.ReadDir
- Filter by prefix match on the partial filename
- Sort alphabetically
- Add trailing "/" to directories
- Limit to 100 results for performance
Algorithm:
- If input is empty or "/", list root-level entries
- Otherwise, split into directory path and partial filename
- Read directory entries, filter by prefix, sort, and format
Parameters:
- input: Current user input (partial path) Examples: "", "/", "/Users/", "/Users/a", "/usr/bin/sta"
Returns:
- []string: List of completion suggestions (max 100 entries)
- Directories have trailing "/" (e.g., "/Users/dev/")
- Files have no trailing slash (e.g., "/usr/bin/stabled")
- Sorted alphabetically
- Empty slice if directory doesn't exist or has no matches
Edge Cases:
- Empty input: Returns root directories (T019: FR-012)
- Non-existent directory: Returns empty slice (EC-002)
- No matches: Returns empty slice
- Permission denied: Returns empty slice (silent failure)
- > 100 matches: Returns first 100 alphabetically (T019: FR-012)
Examples:
Complete("") → ["/Applications/", "/Library/", "/System/", "/Users/", ...]
Complete("/") → ["/Applications/", "/Library/", "/System/", "/Users/", ...]
Complete("/Users/") → ["/Users/alice/", "/Users/bob/", "/Users/Shared/"]
Complete("/Users/a") → ["/Users/alice/"]
Complete("/nonexistent/") → []
type Prompter ¶
type Prompter interface {
// SelectFromList displays an interactive list and returns the selected index.
//
// Parameters:
// - label: Prompt message shown to user (e.g., "Select binary for deployment:")
// - items: List of display strings (e.g., formatted binary metadata)
// - cursorPos: Optional starting cursor position (nil for 0)
//
// Returns:
// - index: Selected item index (0-based)
// - value: Selected item string (items[index])
// - error: promptui.ErrInterrupt if user cancels (Ctrl+C, ESC)
//
// User Interaction:
// - Arrow keys: Navigate up/down
// - Enter: Confirm selection
// - Ctrl+C or ESC: Cancel (returns error)
// - Search: Type to filter (if supported by implementation)
SelectFromList(label string, items []string, cursorPos *int) (index int, value string, err error)
// InputText prompts for text input with a label.
//
// Parameters:
// - label: Prompt message (e.g., "Enter version to build:")
//
// Returns:
// - input: User-entered text
// - error: promptui.ErrInterrupt if user cancels
//
// User Interaction:
// - Type text and press Enter to confirm
// - Ctrl+C to cancel (returns error)
InputText(label string) (input string, err error)
}
Prompter abstracts interactive prompt operations for testing. This interface allows the interactive layer to remain testable by mocking prompt behavior during unit tests.
Design Decision: Minimal interface focused on binary selection use case rather than exposing full promptui capabilities.
The prompter handles two types of interactions:
- List selection with arrow key navigation
- Text input for custom version entry
type PrompterAdapter ¶
type PrompterAdapter struct{}
PrompterAdapter implements Prompter using promptui library. This is the production adapter that provides real interactive prompts.
Note: promptui is already used throughout the codebase (see selector.go), so we're maintaining consistency by reusing the same library.
func NewPrompterAdapter ¶
func NewPrompterAdapter() *PrompterAdapter
NewPrompterAdapter creates a new promptui-based prompter adapter. This is the default implementation used in production.
func (*PrompterAdapter) InputText ¶
func (p *PrompterAdapter) InputText(label string) (string, error)
InputText implements Prompter.InputText using promptui.Prompt.
Implementation details:
- Uses promptui.Prompt for text input
- No validation to allow any text input (branch names, tags, commit hashes)
- Supports Ctrl+C cancellation
func (*PrompterAdapter) SelectFromList ¶
func (p *PrompterAdapter) SelectFromList(label string, items []string, cursorPos *int) (int, string, error)
SelectFromList implements Prompter.SelectFromList using promptui.Select.
Implementation details:
- Uses promptui.Select with custom templates matching existing style
- Search mode disabled by default
- Size limited to 10 items visible at once (scrollable)
type SelectionConfig ¶
type SelectionConfig struct {
Network string // "mainnet" or "testnet"
StartVersion string // Version for devnet binary (used for both export and start)
StartIsCustomRef bool // True if start version is a custom branch/commit
UpgradeName string // Upgrade handler name (for upgrade command only)
// BinarySource represents the user's choice of binary origin (local or GitHub release).
// This field is populated by the unified selection flow (runInteractiveVersionSelection).
// If nil, the selection flow will prompt the user to choose a source.
BinarySource *domain.BinarySource
// SourceSelectionTimestamp records when the source selection was made.
// Used for debugging and audit purposes.
SourceSelectionTimestamp time.Time
// IncludeNetworkSelection controls whether network selection prompt is shown.
// - false for deploy command (network pre-determined from config.toml)
// - true for upgrade command (user selects network interactively)
IncludeNetworkSelection bool
}
SelectionConfig represents the user's selection state during interactive mode for start command. Enhanced to support unified binary source selection (local filesystem or GitHub release).
type Selector ¶
type Selector struct {
// contains filtered or unexported fields
}
Selector handles the interactive selection workflow.
func NewSelector ¶
NewSelector creates a new interactive selector.
func (*Selector) RunSelectionFlow ¶
func (s *Selector) RunSelectionFlow(ctx context.Context) (*SelectionConfig, error)
RunSelectionFlow runs the complete interactive selection workflow for start command. Returns the selection config and any error (including cancellation).
func (*Selector) RunUpgradeSelectionFlow ¶
func (s *Selector) RunUpgradeSelectionFlow(ctx context.Context, skipUpgradeName bool) (*UpgradeSelectionConfig, error)
RunUpgradeSelectionFlow runs the interactive selection workflow for upgrade command. When skipUpgradeName is true (e.g., --skip-gov mode), it skips the upgrade name prompt since there's no governance proposal requiring a handler name. Returns the upgrade selection config and any error (including cancellation).
func (*Selector) RunVersionSelectionFlow ¶
func (s *Selector) RunVersionSelectionFlow(ctx context.Context, network string) (*SelectionConfig, error)
RunVersionSelectionFlow runs version selection workflow with pre-determined network. This is used when network is already configured (e.g., from config.toml or flags). Returns the selection config and any error (including cancellation).
type SourceSelectorAdapter ¶
type SourceSelectorAdapter struct {
}
SourceSelectorAdapter implements the SourceSelector port using promptui. This adapter follows the Adapter pattern from Clean Architecture, translating infrastructure concerns (promptui) to domain concepts (SourceType).
Design Decision: Use promptui.Select for consistency with existing UI patterns. All interactive prompts in the application use promptui for uniform UX.
func NewSourceSelectorAdapter ¶
func NewSourceSelectorAdapter() *SourceSelectorAdapter
NewSourceSelectorAdapter creates a new source selector adapter.
Returns:
- *SourceSelectorAdapter: Ready-to-use source selector
func (*SourceSelectorAdapter) SelectSource ¶
func (s *SourceSelectorAdapter) SelectSource(ctx context.Context) (domain.SourceType, error)
SelectSource prompts the user to choose between local binary and GitHub release. This implements the SourceSelector port interface.
User Flow:
- Check if running in interactive environment (TTY detection)
- If non-interactive → default to GitHub releases with log message
- If interactive → display two options with arrow key navigation
- User navigates with ↑/↓, confirms with Enter, cancels with Ctrl+C/ESC
Parameters:
- ctx: Context for cancellation (currently unused, reserved for future timeout support)
Returns:
- domain.SourceType: The selected source type (Local or GitHubRelease)
- error: promptui.ErrInterrupt/ErrEOF if user cancels, nil on success
Implementation Notes:
- Non-TTY detection uses golang.org/x/term package
- Default option is "GitHub release" (first in list) for convenience
- Cursor starts on first option ("Use GitHub release")
type UpgradeSelectionConfig ¶
type UpgradeSelectionConfig struct {
UpgradeName string // Upgrade handler name
UpgradeVersion string // Target version for upgrade (tag or custom ref)
IsCustomRef bool // True if upgrade version is a custom branch/commit
}
UpgradeSelectionConfig represents the user's selection state during interactive upgrade mode.
type VersionItem ¶
type VersionItem struct {
TagName string
PublishedAt time.Time
IsPrerelease bool
IsLatest bool
IsCustom bool // True for custom branch/commit option
}
VersionItem represents a version item for display in promptui.
func (VersionItem) String ¶
func (v VersionItem) String() string
String returns display string for promptui.