Refactor CLI command wiring with shared settings and runner helpers
This commit is contained in:
@@ -81,6 +81,14 @@ type serveConfig struct {
|
||||
schemaDir string
|
||||
}
|
||||
|
||||
type commonCommandSettings struct {
|
||||
promptDir string
|
||||
profileDir string
|
||||
schemaDir string
|
||||
serverAddr string
|
||||
defaultRenderFormat renderformat.PreparedRunOutputFormat
|
||||
}
|
||||
|
||||
type listFlag []string
|
||||
|
||||
func (l *listFlag) String() string {
|
||||
@@ -125,22 +133,13 @@ func runCommand(args []string, stdout, stderr io.Writer) int {
|
||||
return ExitRuntimeError
|
||||
}
|
||||
|
||||
llmClient, err := llm.NewOpenAICompatibleClient(llm.OpenAICompatibleConfig{
|
||||
Timeout: defaults.LLMRequestTimeoutDefault,
|
||||
})
|
||||
llmClient, err := newOpenAIClient()
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "llm client error: %v\n", err)
|
||||
return ExitRuntimeError
|
||||
}
|
||||
|
||||
runner := usecase.NewRunner(
|
||||
promptdef.NewFilesystemRepository(cfg.promptDir),
|
||||
profile.NewFilesystemRepository(cfg.profileDir),
|
||||
artifactadapter.NewCompositeReader(),
|
||||
prompt.NewGoRenderer(),
|
||||
llmClient,
|
||||
validate.NewStandardValidator(cfg.schemaDir),
|
||||
)
|
||||
runner := newRunner(cfg.promptDir, cfg.profileDir, cfg.schemaDir, llmClient)
|
||||
|
||||
res, runErr := runner.Run(context.Background(), req)
|
||||
if runErr != nil {
|
||||
@@ -170,14 +169,7 @@ func renderCommand(args []string, stdout, stderr io.Writer) int {
|
||||
return ExitRuntimeError
|
||||
}
|
||||
|
||||
runner := usecase.NewRunner(
|
||||
promptdef.NewFilesystemRepository(cfg.promptDir),
|
||||
profile.NewFilesystemRepository(cfg.profileDir),
|
||||
artifactadapter.NewCompositeReader(),
|
||||
prompt.NewGoRenderer(),
|
||||
nil,
|
||||
validate.NewStandardValidator(cfg.schemaDir),
|
||||
)
|
||||
runner := newRunner(cfg.promptDir, cfg.profileDir, cfg.schemaDir, nil)
|
||||
|
||||
prepared, prepErr := runner.Prepare(context.Background(), req)
|
||||
if prepErr != nil {
|
||||
@@ -205,22 +197,13 @@ func serveCommand(args []string, stderr io.Writer) int {
|
||||
return ExitRuntimeError
|
||||
}
|
||||
|
||||
llmClient, err := llm.NewOpenAICompatibleClient(llm.OpenAICompatibleConfig{
|
||||
Timeout: defaults.LLMRequestTimeoutDefault,
|
||||
})
|
||||
llmClient, err := newOpenAIClient()
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "llm client error: %v\n", err)
|
||||
return ExitRuntimeError
|
||||
}
|
||||
|
||||
runner := usecase.NewRunner(
|
||||
promptdef.NewFilesystemRepository(cfg.promptDir),
|
||||
profile.NewFilesystemRepository(cfg.profileDir),
|
||||
artifactadapter.NewCompositeReader(),
|
||||
prompt.NewGoRenderer(),
|
||||
llmClient,
|
||||
validate.NewStandardValidator(cfg.schemaDir),
|
||||
)
|
||||
runner := newRunner(cfg.promptDir, cfg.profileDir, cfg.schemaDir, llmClient)
|
||||
|
||||
h := httpadapter.NewHandler(runner)
|
||||
srv := &http.Server{
|
||||
@@ -307,7 +290,7 @@ func parseServeArgs(args []string) (*serveConfig, error) {
|
||||
return nil, fmt.Errorf("unexpected positional args: %v", fs.Args())
|
||||
}
|
||||
|
||||
settings, err := resolveAppSettings(fs, cfg.configPath, appconfig.CLIOverrides{
|
||||
settings, err := resolveCommonSettings(fs, cfg.configPath, appconfig.CLIOverrides{
|
||||
PromptDir: cfg.promptDirIfSet(fs),
|
||||
ProfileDir: cfg.profileDirIfSet(fs),
|
||||
SchemaDir: cfg.schemaDirIfSet(fs),
|
||||
@@ -317,16 +300,13 @@ func parseServeArgs(args []string) (*serveConfig, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg.promptDir = settings.PromptDir
|
||||
cfg.profileDir = settings.ProfileDir
|
||||
cfg.schemaDir = settings.SchemaDir
|
||||
cfg.addr = settings.ServerAddr
|
||||
cfg.promptDir = settings.promptDir
|
||||
cfg.profileDir = settings.profileDir
|
||||
cfg.schemaDir = settings.schemaDir
|
||||
cfg.addr = settings.serverAddr
|
||||
|
||||
if strings.TrimSpace(cfg.promptDir) == "" {
|
||||
return nil, errors.New(errPromptDirRequired)
|
||||
}
|
||||
if strings.TrimSpace(cfg.profileDir) == "" {
|
||||
return nil, errors.New(errProfileDirRequired)
|
||||
if err := validateRequiredLibraryDirs(cfg.promptDir, cfg.profileDir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg.promptDir = filepath.Clean(cfg.promptDir)
|
||||
@@ -359,7 +339,7 @@ func finalizeExecutionRequestConfig(fs *flag.FlagSet, cfg *runConfig) error {
|
||||
return fmt.Errorf("unexpected positional args: %v", fs.Args())
|
||||
}
|
||||
|
||||
settings, err := resolveAppSettings(fs, cfg.configPath, appconfig.CLIOverrides{
|
||||
settings, err := resolveCommonSettings(fs, cfg.configPath, appconfig.CLIOverrides{
|
||||
PromptDir: cfg.promptDirIfSet(fs),
|
||||
ProfileDir: cfg.profileDirIfSet(fs),
|
||||
SchemaDir: cfg.schemaDirIfSet(fs),
|
||||
@@ -368,16 +348,13 @@ func finalizeExecutionRequestConfig(fs *flag.FlagSet, cfg *runConfig) error {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.promptDir = settings.PromptDir
|
||||
cfg.profileDir = settings.ProfileDir
|
||||
cfg.schemaDir = settings.SchemaDir
|
||||
cfg.defaultRenderFormat = settings.DefaultRenderFormat
|
||||
cfg.promptDir = settings.promptDir
|
||||
cfg.profileDir = settings.profileDir
|
||||
cfg.schemaDir = settings.schemaDir
|
||||
cfg.defaultRenderFormat = settings.defaultRenderFormat
|
||||
|
||||
if strings.TrimSpace(cfg.promptDir) == "" {
|
||||
return errors.New(errPromptDirRequired)
|
||||
}
|
||||
if strings.TrimSpace(cfg.profileDir) == "" {
|
||||
return errors.New(errProfileDirRequired)
|
||||
if err := validateRequiredLibraryDirs(cfg.promptDir, cfg.profileDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(cfg.promptID) == "" {
|
||||
return errors.New("--prompt is required")
|
||||
@@ -476,6 +453,47 @@ func resolveAppSettings(fs *flag.FlagSet, configPath string, overrides appconfig
|
||||
return merged, nil
|
||||
}
|
||||
|
||||
func resolveCommonSettings(fs *flag.FlagSet, configPath string, overrides appconfig.CLIOverrides) (commonCommandSettings, error) {
|
||||
settings, err := resolveAppSettings(fs, configPath, overrides)
|
||||
if err != nil {
|
||||
return commonCommandSettings{}, err
|
||||
}
|
||||
return commonCommandSettings{
|
||||
promptDir: settings.PromptDir,
|
||||
profileDir: settings.ProfileDir,
|
||||
schemaDir: settings.SchemaDir,
|
||||
serverAddr: settings.ServerAddr,
|
||||
defaultRenderFormat: settings.DefaultRenderFormat,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validateRequiredLibraryDirs(promptDir, profileDir string) error {
|
||||
if strings.TrimSpace(promptDir) == "" {
|
||||
return errors.New(errPromptDirRequired)
|
||||
}
|
||||
if strings.TrimSpace(profileDir) == "" {
|
||||
return errors.New(errProfileDirRequired)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newRunner(promptDir, profileDir, schemaDir string, llmClient llm.Client) *usecase.Runner {
|
||||
return usecase.NewRunner(
|
||||
promptdef.NewFilesystemRepository(promptDir),
|
||||
profile.NewFilesystemRepository(profileDir),
|
||||
artifactadapter.NewCompositeReader(),
|
||||
prompt.NewGoRenderer(),
|
||||
llmClient,
|
||||
validate.NewStandardValidator(schemaDir),
|
||||
)
|
||||
}
|
||||
|
||||
func newOpenAIClient() (*llm.OpenAICompatibleClient, error) {
|
||||
return llm.NewOpenAICompatibleClient(llm.OpenAICompatibleConfig{
|
||||
Timeout: defaults.LLMRequestTimeoutDefault,
|
||||
})
|
||||
}
|
||||
|
||||
func buildRunRequestFromConfig(cfg *runConfig) (domain.RunRequest, error) {
|
||||
inputMappings, err := parseMappings(cfg.inputRaw, false)
|
||||
if err != nil {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
@@ -410,6 +411,28 @@ defaults:
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRenderArgsExplicitFormatOverridesConfigDefaultFormat(t *testing.T) {
|
||||
configPath := writeAppConfigFile(t, `
|
||||
prompt_dir: ./from-config/prompts
|
||||
profile_dir: ./from-config/profiles
|
||||
defaults:
|
||||
render_format: json
|
||||
`)
|
||||
|
||||
cfg, err := parseRenderArgs([]string{
|
||||
"--config", configPath,
|
||||
"--prompt", "p",
|
||||
"--input", "a=b",
|
||||
"--format", "text",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected valid args, got %v", err)
|
||||
}
|
||||
if cfg.outputFormat != renderformat.PreparedRunFormatText {
|
||||
t.Fatalf("expected explicit --format text to override config default, got %q", cfg.outputFormat)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseServeArgsWithExplicitConfigLoadsSettingsAndCLIAddrOverrides(t *testing.T) {
|
||||
configPath := writeAppConfigFile(t, `
|
||||
prompt_dir: ./from-config/prompts
|
||||
@@ -471,6 +494,59 @@ server:
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunAndRenderBuildEquivalentRuntimeOverrideRequestsForSharedFlags(t *testing.T) {
|
||||
runCfg, err := parseRunArgs([]string{
|
||||
"--prompt-dir", "./prompts",
|
||||
"--profile-dir", "./profiles",
|
||||
"--prompt", "prompt-1",
|
||||
"--profile", "profile-1",
|
||||
"--input", "transcript=./transcript.md",
|
||||
"--var", "session_date=2026-05-01",
|
||||
"--llm-base-url", "http://localhost:8000/v1",
|
||||
"--model", "model-x",
|
||||
"--temperature", "0.8",
|
||||
"--max-tokens", "123",
|
||||
"--top-p", "0.6",
|
||||
"--timeout", "90s",
|
||||
"--api-key-env", "SCRIPTORIUM_API_KEY",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected valid run args, got %v", err)
|
||||
}
|
||||
|
||||
renderCfg, err := parseRenderArgs([]string{
|
||||
"--prompt-dir", "./prompts",
|
||||
"--profile-dir", "./profiles",
|
||||
"--prompt", "prompt-1",
|
||||
"--profile", "profile-1",
|
||||
"--input", "transcript=./transcript.md",
|
||||
"--var", "session_date=2026-05-01",
|
||||
"--llm-base-url", "http://localhost:8000/v1",
|
||||
"--model", "model-x",
|
||||
"--temperature", "0.8",
|
||||
"--max-tokens", "123",
|
||||
"--top-p", "0.6",
|
||||
"--timeout", "90s",
|
||||
"--api-key-env", "SCRIPTORIUM_API_KEY",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected valid render args, got %v", err)
|
||||
}
|
||||
|
||||
runReq, err := buildRunRequestFromConfig(runCfg)
|
||||
if err != nil {
|
||||
t.Fatalf("expected run request build success, got %v", err)
|
||||
}
|
||||
renderReq, err := buildRunRequestFromConfig(&renderCfg.runConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("expected render request build success, got %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(runReq, renderReq) {
|
||||
t.Fatalf("expected run/render shared flag requests to match.\nrun=%#v\nrender=%#v", runReq, renderReq)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRunArgsFailsClearlyWhenNoEffectivePromptDir(t *testing.T) {
|
||||
configPath := writeAppConfigFile(t, `
|
||||
profile_dir: ./profiles
|
||||
|
||||
Reference in New Issue
Block a user