cli

package
v0.2.0-beta.3 Latest Latest
Warning

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

Go to latest
Published: Sep 4, 2026 License: Apache-2.0 Imports: 63 Imported by: 0

Documentation

Overview

Package cli contains side-effect-free command orchestration for FlowBaton.

Index

Constants

View Source
const (
	ExitOK      = 0
	ExitInvalid = 2

	CheckSyntaxUsage = "usage: flowbaton check-syntax FILE|-\n"
)
View Source
const ExitFailure = 1

ExitFailure is the exit code for a run that was understood and did not pass.

View Source
const ExploreUsage = "usage: flowbaton explore --app ID -p ios|android|web [--device UDID] " +
	"[--driver-port PORT] [--max-tests N] [--max-steps N] [--styles LIST] " +
	"[--state-dir DIR] [--output DIR] [--session-name NAME] [--pilot] [--record] " +
	"[--api-key KEY] [--api-url URL]\n"

ExploreUsage is the one-line usage for the subcommand.

View Source
const TestUsage = "usage: flowbaton test [options] FILE|DIR...\n"

TestUsage is the one-line usage for the subcommand.

Variables

View Source
var ErrExploreNotAssembled = errors.New("explore support is not assembled in this build")

ErrExploreNotAssembled reports that this build has no exploration crew wiring behind the runner's injectable seams.

View Source
var TopLevelSubcommands = []string{
	"check-syntax",
	"test",
	"record",
	"explore",
	"list-devices",
	"start-device",
	"hierarchy",
	"query",
	"bugreport",
	"driver-setup",
	"mcp",
	"serve",
	"db",
	"auth",
	"generate-completion",
}

TopLevelSubcommands is the canonical list of subcommands flowbaton dispatches. generate-completion reads it so a new command shows up in completion the moment it is added here; a test pins the set so dispatch and completion do not silently drift apart.

Functions

func DefaultExploreCrew

func DefaultExploreCrew(deps ExploreDeps) (explore.Crew, error)

DefaultExploreCrew assembles the production role implementations over one open driver. Memory stores live under the configured state directory; the research cache keeps UI maps across sessions.

func DefaultExploreModels

func DefaultExploreModels(getenv func(string) string) (explore.ModelSet, error)

DefaultExploreModels builds the tiered chat models from the process environment through the aiengine provider wall.

func DefaultOutputDirectory

func DefaultOutputDirectory(options TestOptions, home string, now time.Time) string

DefaultOutputDirectory resolves where a run's artifacts go.

specs/03-cli-tooling.md section 1 gives the precedence: an explicit --test-output-dir wins, then --debug-output, then a timestamped directory under the user's home. --flatten-debug-output drops the timestamp segment, which is what a CI job wants when it collects a fixed path.

func ExitCodeFor

func ExitCodeFor(err error) int

ExitCodeFor classifies an error into the documented exit codes. Anything not explicitly a usage error is a failure: a misclassification that returned OK would report a green suite that never ran.

Types

type ArtifactSink

type ArtifactSink struct {
	// contains filtered or unexported fields
}

ArtifactSink writes run artifacts, into one of two directories.

A name the flow author wrote resolves against the process working directory: a flow saying `takeScreenshot: settings` leaves ./settings.png where the operator ran from, whichever directory the flow itself lives in. Everything the author did not name — the automatic failure captures — is the run's own bookkeeping and stays in the run directory.

func NewArtifactSink

func NewArtifactSink(directory, authoredDirectory string) *ArtifactSink

func (*ArtifactSink) Write

type AuthRunner

type AuthRunner struct {
	OpenAdmin func(context.Context, string) (sessionstore.IdentityAdmin, func(), error)
	WriteFile func(string, []byte, os.FileMode) error
}

func (AuthRunner) Run

func (runner AuthRunner) Run(ctx context.Context, args []string, stdout, stderr io.Writer) int

type BugreportRunner

type BugreportRunner struct {
	Collect       func(ctx context.Context, serial, outputPath string) error
	CollectIOS    func(ctx context.Context, udid, outputPath string) error
	ResolveSerial func(context.Context) (string, error)
}

BugreportRunner holds the collection and serial resolution behind fields so a test can record the call without a device. The defaults reach adb / simctl.

func (BugreportRunner) Run

func (runner BugreportRunner) Run(ctx context.Context, args []string, stdout, stderr io.Writer) int

type CheckSyntaxRunner

type CheckSyntaxRunner struct {
	Checker Checker
	Getwd   func() (string, error)
}

CheckSyntaxRunner enforces the command's exact stdout/stderr/exit contract.

func (CheckSyntaxRunner) Run

func (runner CheckSyntaxRunner) Run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) int

type Checker

type Checker interface {
	Check(context.Context, Source) error
}

Checker owns parse plus recursive capability preflight for one selected root. A successful return is the sole authority for printing OK.

type CryptoRandom

type CryptoRandom struct{}

CryptoRandom is the process random source, shared by the JS faker binding and the random-input commands so there is one notion of randomness.

func (CryptoRandom) Intn

func (CryptoRandom) Intn(n int) int

func (CryptoRandom) Read

func (CryptoRandom) Read(p []byte) (int, error)

type DBRunner

type DBRunner struct {
	ApplySchema func(context.Context, string) error
}

func (DBRunner) Run

func (runner DBRunner) Run(ctx context.Context, args []string, stdout, stderr io.Writer) int

type DeviceSession

type DeviceSession struct {
	Driver device.Driver
	// OutputDirectory receives screenshots and other run artifacts.
	OutputDirectory string
	// BaseDirectory resolves flow resources such as runScript files.
	BaseDirectory string
	// Clock is optional; a nil clock uses the wall clock. Tests supply their
	// own so a flow's waits do not take real time.
	Clock engine.Clock
	// ExecutionID is optional; a blank one is derived from the run's own start.
	ExecutionID string
	// Shard is which share of the run this session executes. It is what the
	// reserved FLOWBATON_SHARD_* and FLOWBATON_DEVICE_UDID variables are built
	// from, so a flow can report where it ran.
	Shard Shard
}

DeviceSession runs a prepared program against one device.

It exists to hold the assembly of engine.Dependencies in one place. That assembly is the part where a missing service is invisible: the engine takes every boundary as an interface, so a nil one compiles and then fails inside a flow, where it reads as a flow failure rather than a wiring mistake.

func NewDeviceSession

func NewDeviceSession(ctx context.Context, options TestOptions, shard Shard) (DeviceSession, error)

NewDeviceSession builds a session for one shard, or explains why it cannot.

func (DeviceSession) Execute

func (session DeviceSession) Execute(
	ctx context.Context,
	program *engine.Program,
	options TestOptions,
) ([]engine.FlowResult, error)

Execute opens the device, runs every selected root, and closes the device even when a flow fails.

type DriverRecordingController

type DriverRecordingController struct {
	// contains filtered or unexported fields
}

DriverRecordingController holds the capture id between the two commands.

func NewDriverRecordingController

func NewDriverRecordingController(
	driver screenRecordingDriver, directory string,
) *DriverRecordingController

func (*DriverRecordingController) Close

func (controller *DriverRecordingController) Close(ctx context.Context) error

Close makes DriverRecordingController usable as a cleanup resource while retaining StopAll's artifact-returning form for callers that need evidence.

func (*DriverRecordingController) Start

func (controller *DriverRecordingController) Start(
	ctx context.Context, request engine.RecordingStartRequest,
) error

func (*DriverRecordingController) Stop

func (controller *DriverRecordingController) Stop(ctx context.Context) ([]device.Artifact, error)

func (*DriverRecordingController) StopAll

func (controller *DriverRecordingController) StopAll(
	ctx context.Context,
) ([]device.Artifact, error)

StopAll finalizes the in-flight recording, if any. Unlike authored Stop it is idempotent, so session and driver cleanup can both call it safely.

type DriverSetupRunner

type DriverSetupRunner struct {
	Build func(ctx context.Context, platform string) error
}

DriverSetupRunner builds the driver. The build is a field so a test can record the invocation without running xcodebuild.

func (DriverSetupRunner) Run

func (runner DriverSetupRunner) Run(ctx context.Context, args []string, stdout, stderr io.Writer) int

type ExploreDeps

type ExploreDeps struct {
	Driver device.Driver
	Models explore.ModelSet
	Config explore.Config
	Stdout io.Writer
}

ExploreDeps carries everything crew assembly needs.

type ExploreInvoker

type ExploreInvoker interface {
	Explore(ctx context.Context, options ExploreToolOptions) (ExploreToolResult, error)
}

ExploreInvoker runs one exploration session for the explore tool. An interface rather than a struct-of-funcs so a test injects one fake with the whole behavior.

type ExploreRunner

type ExploreRunner struct {
	// NewModels builds the tiered model set from the environment. Nil means
	// this build carries no model wiring, and the command refuses after flag
	// validation.
	NewModels func(getenv func(string) string) (explore.ModelSet, error)
	// NewCrew assembles the role implementations. Nil refuses like NewModels.
	NewCrew func(deps ExploreDeps) (explore.Crew, error)
	// NewDriver is injected by tests; nil constructs the real platform driver
	// through the same path the test session uses.
	NewDriver func(ctx context.Context, options TestOptions, udid string, port int) (device.Driver, error)
	// Clock is optional; nil uses the wall clock. The default session name
	// and output directory read it.
	Clock engine.Clock
	// Getenv is optional; nil reads the process environment.
	Getenv func(string) string
	// Environ is optional; nil reads the process environment. It feeds the
	// same diagnostic-port fallback `hierarchy` uses.
	Environ func() []string
}

ExploreRunner runs one exploration session end to end: resolve the device, open the driver once, drive the session, write the report and exported flows, close the driver.

func ProductionExploreRunner

func ProductionExploreRunner() ExploreRunner

ProductionExploreRunner returns the fully assembled explore command: chat models from the environment and the production crew over the session driver. The zero ExploreRunner stays refusal-only so tests can assert the unassembled error; the entry point calls this instead.

func (ExploreRunner) Run

func (runner ExploreRunner) Run(ctx context.Context, args []string, stdout, stderr io.Writer) int

Run executes one `explore` invocation and returns its process exit code. A completed session exits 0 even when scenarios failed — the report is the artifact; only run-level failures exit nonzero.

type ExploreToolOptions

type ExploreToolOptions struct {
	AppID      string
	Platform   string
	Device     string
	DriverPort int
	MaxTests   int
	MaxSteps   int
	OutputDir  string
}

ExploreToolOptions is one resolved explore invocation. OutputDir, when set, has already been confined to the MCP base directory.

type ExploreToolResult

type ExploreToolResult struct {
	Report string   `json:"report"`
	Flows  []string `json:"flows"`
}

ExploreToolResult carries the session report markdown and the exported flow paths.

type GenerateCompletionRunner

type GenerateCompletionRunner struct{}

GenerateCompletionRunner emits a shell completion script. Pure: no device, no I/O beyond stdout.

func (GenerateCompletionRunner) Run

func (GenerateCompletionRunner) Run(_ context.Context, args []string, stdout, stderr io.Writer) int

type HierarchyRunner

type HierarchyRunner struct {
	Fetch func(ctx context.Context, platform, udid string, appIDs []string, target string) (device.TreeNode, error)
}

HierarchyRunner holds the tree fetch behind a field so a test can stand in a known tree without a device. The default opens the real driver, snapshots, and closes it.

func (HierarchyRunner) Run

func (runner HierarchyRunner) Run(ctx context.Context, args []string, stdout, stderr io.Writer) int

type ImageChecker

type ImageChecker struct{}

ImageChecker forwards to the shared check rather than repeating it.

func (ImageChecker) Check

type InputGenerator

type InputGenerator struct {
	// contains filtered or unexported fields
}

InputGenerator supplies values for the inputRandom* commands.

func NewInputGenerator

func NewInputGenerator() *InputGenerator

NewInputGenerator builds a generator over the process random source.

func (*InputGenerator) Generate

func (generator *InputGenerator) Generate(
	_ context.Context,
	request engine.InputRequest,
) (string, error)

type ListDevicesRunner

type ListDevicesRunner struct {
	IOS         func(context.Context) ([]ios.Device, error)
	IOSPhysical func(context.Context) ([]iosdevice.Device, error)
	Android     func(context.Context) ([]android.Device, error)
}

ListDevicesRunner holds the listing calls behind fields so a test can stand in a fake without a simulator or an attached phone. The defaults reach real tooling.

func (ListDevicesRunner) Run

func (runner ListDevicesRunner) Run(ctx context.Context, args []string, stdout, stderr io.Writer) int

Run lists devices for the selected platform(s). With no -p it lists both, and a platform whose tooling is absent is a note rather than a failure — the command is most wanted exactly on a machine that has only one SDK. With an explicit -p, that platform's error is the failure the operator asked to hear.

type MCPRunner

type MCPRunner struct {
	Checker     Checker
	ListDevices ListDevicesRunner
	Hierarchy   HierarchyRunner
	Query       QueryRunner
	// RunFlow executes run_flow calls through the same pipeline as the test
	// subcommand. Its zero value builds real device sessions.
	RunFlow TestRunner
	// Screenshot captures one frame from a device. Its zero value opens the
	// real driver, in the same way the hierarchy diagnostic does.
	Screenshot ScreenshotRunner
	// StartDevice boots simulators and launches emulators through the same
	// orchestration as the start-device subcommand. Its zero value reaches
	// simctl and the Android tooling.
	StartDevice StartDeviceRunner
	// Explore serves the explore tool. Nil adapts the real ExploreRunner,
	// which refuses until its crew wiring is assembled.
	Explore ExploreInvoker
	// BaseDir confines inline flow links exposed through MCP. Run fills it from
	// --base-dir (or the current working directory).
	BaseDir string
}

MCPRunner builds and serves the flowbaton MCP server over stdio. Its dependencies are fields so a test can drive the same server through an in-memory transport.

func (MCPRunner) Run

func (runner MCPRunner) Run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) int

type ParserChecker

type ParserChecker struct {
	Preflight Preflight
}

ParserChecker parses the selected root before invoking recursive preflight. A missing preflight fails closed and can never produce a successful check.

func NewParserChecker

func NewParserChecker() ParserChecker

NewParserChecker returns the production syntax checker. Parsing is followed by recursive capability analysis before a syntax check can succeed.

func (ParserChecker) Check

func (checker ParserChecker) Check(ctx context.Context, source Source) error

type Preflight

type Preflight interface {
	Check(context.Context, Source, model.Flow) error
}

Preflight is the narrow integration boundary required from the recursive graph/capability package. It receives the parsed root and the cwd resolution context, and must remain side-effect-free.

type QueryRunner

type QueryRunner struct {
	Fetch func(ctx context.Context, platform, udid, appID, expression string) ([]device.TreeNode, error)
}

QueryRunner holds the query behind a field so a test can match without a device. The default opens the real driver, queries, and closes it.

func (QueryRunner) Run

func (runner QueryRunner) Run(ctx context.Context, args []string, stdout, stderr io.Writer) int

type RecordRunner

type RecordRunner struct {
	NewSession SessionFactory
}

RecordRunner records one flow. The session hook is the same one TestRunner takes, so a test drives it without a device.

func (RecordRunner) Run

func (runner RecordRunner) Run(
	ctx context.Context, args []string, stdout, stderr io.Writer,
) int

type ResourceReader

type ResourceReader struct {
	// contains filtered or unexported fields
}

ResourceReader reads flow resources confined to one base directory.

func NewResourceReader

func NewResourceReader(baseDirectory string) *ResourceReader

func (*ResourceReader) Read

type ScreenshotRunner

type ScreenshotRunner struct {
	Fetch func(ctx context.Context, platform, udid string) ([]byte, error)
}

ScreenshotRunner holds the frame capture behind a field so a test can stand in known bytes without a device. The default opens the real driver, captures one uncompressed frame, and closes it.

type ServeBootstrap

type ServeBootstrap struct {
	OpenStore      func(context.Context, string) (serveRuntimeStore, error)
	BuildDriver    func(context.Context, serveDevice) (device.Driver, error)
	LoadTLS        func(string, string, string) (*tls.Config, error)
	LoadPrivateKey func(string, string) (string, ed25519.PrivateKey, error)
	RunServer      func(context.Context, server.RuntimeConfig) error
	Executable     func() (string, error)
	ProcessID      func() int
}

func (ServeBootstrap) Run

func (bootstrap ServeBootstrap) Run(ctx context.Context, options ServeOptions) (resultErr error)

type ServeOptions

type ServeOptions struct {
	Address           string
	DatabaseURL       string
	TLSCertificate    string
	TLSPrivateKey     string
	ClientCA          string
	SigningKey        string
	SigningKeyID      string
	NodeID            string
	PublicAddress     string
	Inventory         string
	WorkerConcurrency int
	WorkerPoll        time.Duration
	WorkerClaim       time.Duration
	WorkerTimeout     time.Duration
	NodeHeartbeat     time.Duration
}

type ServeRunner

type ServeRunner struct {
	Serve func(context.Context, ServeOptions) error
}

ServeRunner parses the public serve command and delegates construction to the runtime bootstrap owned by main. The field keeps secrets and listeners out of argument parsing tests.

func DefaultServeRunner

func DefaultServeRunner() ServeRunner

func (ServeRunner) Run

func (runner ServeRunner) Run(ctx context.Context, args []string, stdout, stderr io.Writer) int

type SessionFactory

type SessionFactory func(context.Context, Shard, TestOptions) (TestSession, error)

SessionFactory builds the session for one shard.

type Shard

type Shard struct {
	// Index is 0-based; the human-facing number is Index+1.
	Index int
	// Roots are the flow paths this shard runs, in the plan's own order.
	Roots []string
	// Device is the udid this shard runs on. It is blank for an unsharded run
	// with no --device, so the driver keeps saying which flag is missing.
	Device string
	// OutputDirectory is where this shard's artifacts go. The runner fills it
	// in after planning; planning itself has no opinion about the filesystem.
	OutputDirectory string
	// DriverPort is the loopback port this shard's driver talks to. Shard 1
	// keeps the contract's port; later shards get one each, because two shards
	// on one port drive the same runner.
	DriverPort int
}

Shard is one device's share of a run.

func PlanShards

func PlanShards(ctx context.Context, options TestOptions, plan workspace.Plan) ([]Shard, error)

PlanShards divides a discovered plan into per-device shards.

func (Shard) Count

func (shard Shard) Count() int

Count is the human-facing shard number, matching the `shard-N` directories.

type Source

type Source struct {
	Name    string
	BaseDir string
	// ConfineTo, when non-empty, requires every linked flow to remain beneath
	// this directory after symlink evaluation. The CLI leaves it empty because
	// an operator-selected flow may intentionally link elsewhere; MCP sets it to
	// --base-dir because inline content is untrusted client input.
	ConfineTo string
	Data      []byte
}

Source is one selected syntax-check root. BaseDir is always the invocation working directory; this makes links from stdin resolve from cwd without changing their user-facing source name from "-".

func (source Source) ResolveLink(path string) string

ResolveLink resolves a parser-produced link against the invocation working directory. Absolute links remain absolute.

type StartDeviceRunner

type StartDeviceRunner struct {
	// Boot boots an existing iOS simulator by udid (default: simctl boot).
	Boot func(ctx context.Context, platform, udid string) error
	// ListAVDs enumerates installed Android AVD names (default: emulator -list-avds).
	ListAVDs func(ctx context.Context) ([]string, error)
	// LaunchAVD launches an Android AVD by name (default: emulator -avd <name>).
	LaunchAVD func(ctx context.Context, avd, locale string) error
	// CreateSim creates a new iOS simulator and returns its udid (default: simctl create).
	CreateSim func(ctx context.Context, options deviceCreateOptions) (string, error)
	// CreateAVD creates a new Android AVD and returns its name (default: avdmanager create).
	CreateAVD func(ctx context.Context, options deviceCreateOptions) (string, error)
	// WaitReady blocks until the started target is usable (default: simctl
	// bootstatus or adb's sys.boot_completed property).
	WaitReady func(ctx context.Context, platform, target string) error
	// ReadyTimeout bounds readiness independently from a long-lived parent.
	ReadyTimeout time.Duration
	// ConfigureLocale applies the requested locale after an iOS simulator is
	// booted. Android receives its locale as an emulator launch property.
	ConfigureLocale func(ctx context.Context, platform, target, locale string) error
}

StartDeviceRunner holds each external operation behind a field so tests can record calls without a real device. The defaults reach simctl/emulator.

func (StartDeviceRunner) Run

func (runner StartDeviceRunner) Run(ctx context.Context, args []string, stdout, stderr io.Writer) int

type TestOptions

type TestOptions struct {
	Roots []string

	ConfigPath  string
	Env         map[string]string
	IncludeTags []string
	ExcludeTags []string

	Format        string
	TestSuiteName string
	Output        string

	DebugOutput        string
	TestOutputDir      string
	FlattenDebugOutput bool
	Continuous         bool
	Headless           bool
	ReinstallDriver    bool
	ScreenSize         string
	APIURL             string
	APIKey             string
	Platform           string
	Devices            []string
	ShardSplit         int
	ShardAll           int

	// RecordTo is where `record` wants the video. It is NOT parsed from a
	// command line: the `test` command has no such flag. RecordRunner sets it.
	RecordTo string

	// SequencedRoots and ContinueOnFailure come from the workspace's
	// executionOrder, not from argv. They decide whether a failed flow ends
	// the suite; see engine.Dependencies for the rule.
	SequencedRoots    int
	ContinueOnFailure bool
	// contains filtered or unexported fields
}

TestOptions is one parsed `test` command line.

func ParseTestOptions

func ParseTestOptions(args []string) (TestOptions, error)

ParseTestOptions reads a `test` command line.

Flags and positionals may interleave. Boolean flags never consume the following argument — doing so would silently drop a flow from the run, which is the worst way for a suite to pass.

type TestRunner

type TestRunner struct {
	// NewSession acquires one shard's device and executes its flows. It is
	// injected because no production session can be built without a device; a
	// nil factory falls back to the real one.
	NewSession SessionFactory
	Loader     capability.FlowLoader
	// Clock is optional; nil uses the wall clock. It fixes the run's own notion
	// of now, which both the default output directory and the report timestamp
	// read, so two identical runs render identical bytes.
	Clock engine.Clock
	// Environ is optional; nil reads the process environment. It supplies the
	// shell's FLOWBATON_ variables, per specs/01-core-engine.md:101.
	Environ func() []string
	// AllocatePort is optional; nil asks the operating system for an ephemeral
	// port. Injected so a test can pin the ports a sharded run picks.
	AllocatePort func() (int, error)
	// PollInterval is how often continuous mode re-stamps its watch set; zero
	// uses defaultPollInterval. Injected so a test does not wait on it.
	PollInterval time.Duration
	// RecordTo is set by RecordRunner, never by argv. See TestOptions.RecordTo.
	RecordTo string
}

TestRunner discovers flows, preflights them, splits them into shards, and runs each shard on its own device.

Discovery, preflight, and shard planning all run BEFORE anything touches a device. That order is the point: a config error, an unknown command, a tag filter that selects nothing, or a shard count with no devices behind it are all knowable without a simulator, and finding them after devices have been acquired wastes the slowest part of a run and reports a setup mistake as a flow failure.

func (TestRunner) Run

func (runner TestRunner) Run(
	ctx context.Context,
	args []string,
	stdout io.Writer,
	stderr io.Writer,
) int

Run executes one `test` invocation and returns its process exit code.

func (TestRunner) RunOptions

func (runner TestRunner) RunOptions(
	ctx context.Context,
	options TestOptions,
	stdout io.Writer,
	stderr io.Writer,
) int

RunOptions is Run past its argument parsing. `record` uses it: it has to read the positionals itself (a flow plus an optional output file), so it parses the same options and then hands them over rather than rebuilding a command line for this to parse a second time.

type TestSession

type TestSession interface {
	Execute(context.Context, *engine.Program, TestOptions) ([]engine.FlowResult, error)
}

TestSession is the boundary between the host pipeline and a device.

type UnsupportedRecordingController

type UnsupportedRecordingController struct{}

UnsupportedRecordingController refuses both halves of the recording lifecycle.

It is what a driver without the stop half gets. Reporting success and producing no file would tell a run it has a recording it does not have.

func (UnsupportedRecordingController) Start

func (UnsupportedRecordingController) Stop

type UsageError

type UsageError struct {
	Message string
}

UsageError marks a command line that could not be understood. It is the only thing that exits 2; everything else exits 1.

func (*UsageError) Error

func (err *UsageError) Error() string

Jump to

Keyboard shortcuts

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