interactive

package
v1.0.0-rc0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 20, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package interactive provides infrastructure adapter for interactive prompts.

Index

Constants

This section is empty.

Variables

View Source
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

func ConfirmReplaceSelection(version string) (bool, error)

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

func IsCancellation(err error) bool

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

func PromptUpgradeName(defaultName string) (string, error)

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

func SelectNetwork() (string, error)

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 NewAdapter

func NewAdapter() *Adapter

NewAdapter creates a new interactive adapter.

func (*Adapter) ConfirmAction

func (a *Adapter) ConfirmAction(message string) (bool, error)

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

func (a *Adapter) PromptUpgradeName(defaultName string) (string, error)

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

func (a *Adapter) SelectNetwork() (string, error)

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:

  1. Check for edge cases (zero, single, non-TTY)
  2. Display formatted list with arrow key navigation
  3. User selects binary or "Build from source"
  4. If build selected, prompt for version/ref
  5. 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:

  1. Implementing the port interface defined in the application layer
  2. 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:

  1. Display interactive input with auto-completion
  2. User types path with tab completion
  3. User presses Tab to see auto-completion suggestions
  4. User presses Enter to select a file
  5. Validate selected path: - File exists (not directory) - File is executable (mode & 0111 != 0) - Symlinks are resolved (max depth 10)
  6. If validation fails, show error and allow retry
  7. 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

type NetworkOption struct {
	Name        string
	Description string
}

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:

  1. Parse input into directory + partial filename
  2. Handle special cases (empty input, root directory)
  3. List directory contents using os.ReadDir
  4. Filter by prefix match on the partial filename
  5. Sort alphabetically
  6. Add trailing "/" to directories
  7. 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:

  1. List selection with arrow key navigation
  2. 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

func NewSelector(client *github.Client) *Selector

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:

  1. Check if running in interactive environment (TTY detection)
  2. If non-interactive → default to GitHub releases with log message
  3. If interactive → display two options with arrow key navigation
  4. 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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL