Allow CLI paths to come from config

This commit is contained in:
2026-05-06 16:48:35 +00:00
parent 0fca5611d6
commit 7be543a541
2 changed files with 203 additions and 7 deletions

View File

@@ -32,6 +32,11 @@ const (
ExitValidationFailed = 2
)
const (
errPromptDirRequired = "prompt directory is required; provide --prompt-dir or config.yml prompt_dir"
errProfileDirRequired = "profile directory is required; provide --profile-dir or config.yml profile_dir"
)
type runConfig struct {
configPath string
@@ -318,10 +323,10 @@ func parseServeArgs(args []string) (*serveConfig, error) {
cfg.addr = settings.ServerAddr
if strings.TrimSpace(cfg.promptDir) == "" {
return nil, errors.New("--prompt-dir is required")
return nil, errors.New(errPromptDirRequired)
}
if strings.TrimSpace(cfg.profileDir) == "" {
return nil, errors.New("--profile-dir is required")
return nil, errors.New(errProfileDirRequired)
}
cfg.promptDir = filepath.Clean(cfg.promptDir)
@@ -369,10 +374,10 @@ func finalizeExecutionRequestConfig(fs *flag.FlagSet, cfg *runConfig) error {
cfg.defaultRenderFormat = settings.DefaultRenderFormat
if strings.TrimSpace(cfg.promptDir) == "" {
return errors.New("--prompt-dir is required")
return errors.New(errPromptDirRequired)
}
if strings.TrimSpace(cfg.profileDir) == "" {
return errors.New("--profile-dir is required")
return errors.New(errProfileDirRequired)
}
if strings.TrimSpace(cfg.promptID) == "" {
return errors.New("--prompt is required")
@@ -592,7 +597,7 @@ func printSummary(stderr io.Writer, res *domain.RunResult) {
func printUsage(w io.Writer) {
fmt.Fprintln(w, "usage: scriptorium <run|render|serve> ...")
fmt.Fprintln(w, " run: scriptorium run [--config PATH] --prompt-dir DIR --profile-dir DIR --prompt ID --input name=path [--input ...] [--profile ID] [--llm-base-url URL] [--model NAME] [--api-key-env ENV] [--temperature N] [--max-tokens N] [--top-p N] [--var k=v] [--out path] [--timeout 10m]")
fmt.Fprintln(w, " render: scriptorium render [--config PATH] --prompt-dir DIR --profile-dir DIR --prompt ID --input name=path [--input ...] [--profile ID] [--llm-base-url URL] [--model NAME] [--api-key-env ENV] [--temperature N] [--max-tokens N] [--top-p N] [--var k=v] [--format text|json] [--out path] [--timeout 10m]")
fmt.Fprintf(w, " serve: scriptorium serve [--config PATH] --addr %s --prompt-dir DIR --profile-dir DIR [--schema-dir DIR]\n", defaults.HTTPAddrDefault)
fmt.Fprintln(w, " run: scriptorium run [--config PATH] [--prompt-dir DIR] [--profile-dir DIR] --prompt ID --input name=path [--input ...] [--profile ID] [--llm-base-url URL] [--model NAME] [--api-key-env ENV] [--temperature N] [--max-tokens N] [--top-p N] [--var k=v] [--out path] [--timeout 10m]")
fmt.Fprintln(w, " render: scriptorium render [--config PATH] [--prompt-dir DIR] [--profile-dir DIR] --prompt ID --input name=path [--input ...] [--profile ID] [--llm-base-url URL] [--model NAME] [--api-key-env ENV] [--temperature N] [--max-tokens N] [--top-p N] [--var k=v] [--format text|json] [--out path] [--timeout 10m]")
fmt.Fprintf(w, " serve: scriptorium serve [--config PATH] [--addr %s] [--prompt-dir DIR] [--profile-dir DIR] [--schema-dir DIR]\n", defaults.HTTPAddrDefault)
}

View File

@@ -69,11 +69,17 @@ func TestParseRunArgsRequiredFlags(t *testing.T) {
if err == nil {
t.Fatal("expected missing --prompt-dir error")
}
if !strings.Contains(err.Error(), "prompt directory is required") {
t.Fatalf("expected clear prompt-dir guidance, got %v", err)
}
_, err = parseRunArgs([]string{"--config", configPath, "--prompt-dir", "./prompts", "--prompt", "p", "--input", "a=b"})
if err == nil {
t.Fatal("expected missing --profile-dir error")
}
if !strings.Contains(err.Error(), "profile directory is required") {
t.Fatalf("expected clear profile-dir guidance, got %v", err)
}
_, err = parseRunArgs([]string{"--config", configPath, "--prompt-dir", "./prompts", "--profile-dir", "./profiles", "--input", "a=b"})
if err == nil {
@@ -150,11 +156,17 @@ func TestParseServeArgsRequiredFlags(t *testing.T) {
if err == nil {
t.Fatal("expected missing --prompt-dir error")
}
if !strings.Contains(err.Error(), "prompt directory is required") {
t.Fatalf("expected clear prompt-dir guidance, got %v", err)
}
_, err = parseServeArgs([]string{"--config", configPath, "--prompt-dir", "./prompts"})
if err == nil {
t.Fatal("expected missing --profile-dir error")
}
if !strings.Contains(err.Error(), "profile directory is required") {
t.Fatalf("expected clear profile-dir guidance, got %v", err)
}
cfg, err := parseServeArgs([]string{"--config", configPath, "--prompt-dir", "./prompts", "--profile-dir", "./profiles"})
if err != nil {
@@ -413,6 +425,108 @@ server:
}
}
func TestParseServeArgsWithConfigProvidesRequiredDirectoriesAndAddr(t *testing.T) {
configPath := writeAppConfigFile(t, `
prompt_dir: ./from-config/prompts
profile_dir: ./from-config/profiles
schema_dir: ./from-config/schemas
server:
addr: 127.0.0.1:9000
`)
cfg, err := parseServeArgs([]string{
"--config", configPath,
})
if err != nil {
t.Fatalf("expected valid args, got %v", err)
}
if cfg.promptDir != filepath.Clean("./from-config/prompts") {
t.Fatalf("expected prompt dir from config, got %q", cfg.promptDir)
}
if cfg.profileDir != filepath.Clean("./from-config/profiles") {
t.Fatalf("expected profile dir from config, got %q", cfg.profileDir)
}
if cfg.schemaDir != filepath.Clean("./from-config/schemas") {
t.Fatalf("expected schema dir from config, got %q", cfg.schemaDir)
}
if cfg.addr != "127.0.0.1:9000" {
t.Fatalf("expected addr from config, got %q", cfg.addr)
}
}
func TestParseRunArgsFailsClearlyWhenNoEffectivePromptDir(t *testing.T) {
configPath := writeAppConfigFile(t, `
profile_dir: ./profiles
`)
_, err := parseRunArgs([]string{
"--config", configPath,
"--prompt", "p",
"--input", "a=b",
})
if err == nil {
t.Fatal("expected missing prompt_dir error")
}
if !strings.Contains(err.Error(), "prompt directory is required") || !strings.Contains(err.Error(), "config.yml prompt_dir") {
t.Fatalf("expected clear prompt_dir guidance, got %v", err)
}
}
func TestParseRunArgsFailsClearlyWhenNoEffectiveProfileDir(t *testing.T) {
configPath := writeAppConfigFile(t, `
prompt_dir: ./prompts
`)
_, err := parseRunArgs([]string{
"--config", configPath,
"--prompt", "p",
"--input", "a=b",
})
if err == nil {
t.Fatal("expected missing profile_dir error")
}
if !strings.Contains(err.Error(), "profile directory is required") || !strings.Contains(err.Error(), "config.yml profile_dir") {
t.Fatalf("expected clear profile_dir guidance, got %v", err)
}
}
func TestParseRenderArgsFailsClearlyWhenNoEffectivePromptDir(t *testing.T) {
configPath := writeAppConfigFile(t, `
profile_dir: ./profiles
`)
_, err := parseRenderArgs([]string{
"--config", configPath,
"--prompt", "p",
"--input", "a=b",
})
if err == nil {
t.Fatal("expected missing prompt_dir error")
}
if !strings.Contains(err.Error(), "prompt directory is required") || !strings.Contains(err.Error(), "config.yml prompt_dir") {
t.Fatalf("expected clear prompt_dir guidance, got %v", err)
}
}
func TestParseRenderArgsFailsClearlyWhenNoEffectiveProfileDir(t *testing.T) {
configPath := writeAppConfigFile(t, `
prompt_dir: ./prompts
`)
_, err := parseRenderArgs([]string{
"--config", configPath,
"--prompt", "p",
"--input", "a=b",
})
if err == nil {
t.Fatal("expected missing profile_dir error")
}
if !strings.Contains(err.Error(), "profile directory is required") || !strings.Contains(err.Error(), "config.yml profile_dir") {
t.Fatalf("expected clear profile_dir guidance, got %v", err)
}
}
func TestDetermineExitCode(t *testing.T) {
if got := determineExitCode(errors.New("boom"), nil); got != ExitRuntimeError {
t.Fatalf("expected runtime exit code, got %d", got)
@@ -455,6 +569,46 @@ func TestRunCommandVarsOptional(t *testing.T) {
}
}
func TestRunCommandSucceedsWithPromptAndProfileDirsFromConfig(t *testing.T) {
tmp := t.TempDir()
promptDir := filepath.Join(tmp, "prompts")
profileDir := filepath.Join(tmp, "profiles")
if err := os.MkdirAll(promptDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(profileDir, 0o755); err != nil {
t.Fatal(err)
}
inputPath := filepath.Join(tmp, "transcript.md")
if err := os.WriteFile(inputPath, []byte("hello"), 0o644); err != nil {
t.Fatal(err)
}
ts := newTestLLMServer("from-config-dirs", nil)
defer ts.Close()
writePromptFile(t, promptDir, "prompt.default", "local-default")
writeProfileFile(t, profileDir, "local-default", ts.URL+"/v1", "profile-model")
configPath := writeAppConfigFile(t, fmt.Sprintf(`
prompt_dir: %s
profile_dir: %s
`, promptDir, profileDir))
var stdout bytes.Buffer
var stderr bytes.Buffer
code := runCommand([]string{
"--config", configPath,
"--prompt", "prompt.default",
"--input", "transcript=" + inputPath,
}, &stdout, &stderr)
if code != ExitOK {
t.Fatalf("expected ExitOK, got %d stderr=%q", code, stderr.String())
}
if stdout.String() != "from-config-dirs" {
t.Fatalf("unexpected stdout output: %q", stdout.String())
}
}
func TestRenderCommandDefaultFormatTextIncludesPreparedDetailsAndNoSecrets(t *testing.T) {
const envName = "SCRIPTORIUM_RENDER_TEST_API_KEY"
const secret = "super-secret-render-key"
@@ -527,6 +681,43 @@ func TestRenderCommandDefaultFormatTextIncludesPreparedDetailsAndNoSecrets(t *te
}
}
func TestRenderCommandSucceedsWithPromptAndProfileDirsFromConfig(t *testing.T) {
tmp := t.TempDir()
promptDir := filepath.Join(tmp, "prompts")
profileDir := filepath.Join(tmp, "profiles")
if err := os.MkdirAll(promptDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(profileDir, 0o755); err != nil {
t.Fatal(err)
}
inputPath := filepath.Join(tmp, "transcript.md")
if err := os.WriteFile(inputPath, []byte("hello transcript"), 0o644); err != nil {
t.Fatal(err)
}
writePromptFile(t, promptDir, "prompt.render", "local-default")
writeProfileFile(t, profileDir, "local-default", "http://127.0.0.1:1/v1", "profile-model")
configPath := writeAppConfigFile(t, fmt.Sprintf(`
prompt_dir: %s
profile_dir: %s
`, promptDir, profileDir))
var stdout bytes.Buffer
var stderr bytes.Buffer
code := renderCommand([]string{
"--config", configPath,
"--prompt", "prompt.render",
"--input", "transcript=" + inputPath,
}, &stdout, &stderr)
if code != ExitOK {
t.Fatalf("expected ExitOK, got %d stderr=%q", code, stderr.String())
}
if !strings.Contains(stdout.String(), "prompt: prompt.render") {
t.Fatalf("expected rendered output, got %q", stdout.String())
}
}
func TestRenderCommandExplicitTextFormatWorks(t *testing.T) {
tmp := t.TempDir()
promptDir := filepath.Join(tmp, "prompts")