156 lines
4.8 KiB
Go
156 lines
4.8 KiB
Go
// Package cli implements the ALT operator CLI dispatch. It is a thin command
|
|
// layer: argument parsing and exit-code/output contracts live here, while the
|
|
// scenario schema, validation, and run execution live in the operator package.
|
|
package cli
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
|
|
"git.toki-labs.com/toki/alt/apps/cli/internal/operator"
|
|
)
|
|
|
|
// Exit codes form the CLI contract consumed by scripts and tests.
|
|
const (
|
|
exitOK = 0 // success
|
|
exitUsage = 1 // usage or unknown-command error
|
|
exitParseError = 2 // scenario parse/validation/flag failure
|
|
)
|
|
|
|
// Run dispatches the operator CLI and returns a process exit code. stdout
|
|
// carries machine-readable status lines; stderr carries human-readable
|
|
// usage and error detail.
|
|
func Run(args []string, stdout, stderr io.Writer) int {
|
|
if len(args) == 0 {
|
|
usage(stderr)
|
|
return exitUsage
|
|
}
|
|
|
|
switch args[0] {
|
|
case "version":
|
|
fmt.Fprintln(stdout, "alt dev")
|
|
return exitOK
|
|
case "operator":
|
|
return runOperator(args[1:], stdout, stderr)
|
|
default:
|
|
fmt.Fprintf(stderr, "unknown command %q\n", args[0])
|
|
usage(stderr)
|
|
return exitUsage
|
|
}
|
|
}
|
|
|
|
// runOperator routes the `operator scenario <validate|run>` subcommands.
|
|
func runOperator(args []string, stdout, stderr io.Writer) int {
|
|
if len(args) < 1 || args[0] != "scenario" {
|
|
usageOperator(stderr)
|
|
return exitUsage
|
|
}
|
|
if len(args) < 2 {
|
|
usageOperator(stderr)
|
|
return exitUsage
|
|
}
|
|
|
|
switch args[1] {
|
|
case "validate":
|
|
return runScenarioValidate(args[2:], stdout, stderr)
|
|
case "run":
|
|
return runScenarioRun(args[2:], stdout, stderr)
|
|
default:
|
|
usageOperator(stderr)
|
|
return exitUsage
|
|
}
|
|
}
|
|
|
|
// runScenarioValidate handles `operator scenario validate --file <path>`.
|
|
func runScenarioValidate(args []string, stdout, stderr io.Writer) int {
|
|
fs := flag.NewFlagSet("operator scenario validate", flag.ContinueOnError)
|
|
fs.SetOutput(stderr)
|
|
file := fs.String("file", "", "path to operator scenario YAML file")
|
|
if err := fs.Parse(args); err != nil {
|
|
return exitUsage
|
|
}
|
|
if *file == "" {
|
|
fmt.Fprintln(stderr, "scenario file is required: --file <path>")
|
|
return exitUsage
|
|
}
|
|
|
|
sc, err := operator.LoadScenario(*file)
|
|
if err != nil {
|
|
fmt.Fprintln(stdout, "scenario=unknown status=parse_error")
|
|
fmt.Fprintf(stderr, "validate failed: %v\n", err)
|
|
return exitParseError
|
|
}
|
|
|
|
runs, err := sc.MatrixRuns()
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "validate failed: %v\n", err)
|
|
return exitParseError
|
|
}
|
|
if len(runs) > 0 {
|
|
fmt.Fprintf(stdout, "scenario=%s status=valid steps=%d matrix_runs=%d\n", sc.Name, len(sc.Steps), len(runs))
|
|
return exitOK
|
|
}
|
|
|
|
fmt.Fprintf(stdout, "scenario=%s status=valid steps=%d\n", sc.Name, len(sc.Steps))
|
|
return exitOK
|
|
}
|
|
|
|
// runScenarioRun handles `operator scenario run --file <path> --api-url <ws-url>`.
|
|
// It returns the runner's exit code (0 ok, 1 typed/expectation failure, 2
|
|
// parse/flag error, 3 transport failure).
|
|
func runScenarioRun(args []string, stdout, stderr io.Writer) int {
|
|
fs := flag.NewFlagSet("operator scenario run", flag.ContinueOnError)
|
|
fs.SetOutput(stderr)
|
|
file := fs.String("file", "", "path to operator scenario YAML file")
|
|
apiURL := fs.String("api-url", "", "API socket URL, e.g. ws://127.0.0.1:18030/socket")
|
|
timeout := fs.Duration("timeout", 0, "per-step request timeout; overrides the scenario timeout")
|
|
output := fs.String("output", string(operator.OutputText), "output format: text or jsonl")
|
|
if err := fs.Parse(args); err != nil {
|
|
return exitParseError
|
|
}
|
|
if *file == "" {
|
|
fmt.Fprintln(stderr, "scenario file is required: --file <path>")
|
|
return exitParseError
|
|
}
|
|
if *apiURL == "" {
|
|
fmt.Fprintln(stderr, "api url is required: --api-url <ws-url>")
|
|
return exitParseError
|
|
}
|
|
|
|
format := operator.OutputFormat(*output)
|
|
if format != operator.OutputText && format != operator.OutputJSONL {
|
|
fmt.Fprintf(stderr, "unknown output format %q (want text or jsonl)\n", *output)
|
|
return exitParseError
|
|
}
|
|
|
|
sc, err := operator.LoadScenario(*file)
|
|
if err != nil {
|
|
fmt.Fprintln(stdout, "scenario=unknown status=parse_error")
|
|
fmt.Fprintf(stderr, "run failed: %v\n", err)
|
|
return exitParseError
|
|
}
|
|
|
|
return operator.RunScenario(context.Background(), sc, operator.RunOptions{
|
|
APIURL: *apiURL,
|
|
Timeout: *timeout,
|
|
Format: format,
|
|
}, stdout)
|
|
}
|
|
|
|
// usage prints the top-level command summary.
|
|
func usage(w io.Writer) {
|
|
fmt.Fprintln(w, "usage: alt <command>")
|
|
fmt.Fprintln(w, "commands:")
|
|
fmt.Fprintln(w, " version")
|
|
fmt.Fprintln(w, " operator scenario validate --file <path>")
|
|
fmt.Fprintln(w, " operator scenario run --file <path> --api-url <ws-url> [--timeout <dur>] [--output text|jsonl]")
|
|
}
|
|
|
|
// usageOperator prints the operator subcommand summary.
|
|
func usageOperator(w io.Writer) {
|
|
fmt.Fprintln(w, "usage:")
|
|
fmt.Fprintln(w, " alt operator scenario validate --file <path>")
|
|
fmt.Fprintln(w, " alt operator scenario run --file <path> --api-url <ws-url> [--timeout <dur>] [--output text|jsonl]")
|
|
}
|