Allow CLI paths to come from config
This commit is contained in:
@@ -32,6 +32,11 @@ const (
|
|||||||
ExitValidationFailed = 2
|
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 {
|
type runConfig struct {
|
||||||
configPath string
|
configPath string
|
||||||
|
|
||||||
@@ -318,10 +323,10 @@ func parseServeArgs(args []string) (*serveConfig, error) {
|
|||||||
cfg.addr = settings.ServerAddr
|
cfg.addr = settings.ServerAddr
|
||||||
|
|
||||||
if strings.TrimSpace(cfg.promptDir) == "" {
|
if strings.TrimSpace(cfg.promptDir) == "" {
|
||||||
return nil, errors.New("--prompt-dir is required")
|
return nil, errors.New(errPromptDirRequired)
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(cfg.profileDir) == "" {
|
if strings.TrimSpace(cfg.profileDir) == "" {
|
||||||
return nil, errors.New("--profile-dir is required")
|
return nil, errors.New(errProfileDirRequired)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.promptDir = filepath.Clean(cfg.promptDir)
|
cfg.promptDir = filepath.Clean(cfg.promptDir)
|
||||||
@@ -369,10 +374,10 @@ func finalizeExecutionRequestConfig(fs *flag.FlagSet, cfg *runConfig) error {
|
|||||||
cfg.defaultRenderFormat = settings.DefaultRenderFormat
|
cfg.defaultRenderFormat = settings.DefaultRenderFormat
|
||||||
|
|
||||||
if strings.TrimSpace(cfg.promptDir) == "" {
|
if strings.TrimSpace(cfg.promptDir) == "" {
|
||||||
return errors.New("--prompt-dir is required")
|
return errors.New(errPromptDirRequired)
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(cfg.profileDir) == "" {
|
if strings.TrimSpace(cfg.profileDir) == "" {
|
||||||
return errors.New("--profile-dir is required")
|
return errors.New(errProfileDirRequired)
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(cfg.promptID) == "" {
|
if strings.TrimSpace(cfg.promptID) == "" {
|
||||||
return errors.New("--prompt is required")
|
return errors.New("--prompt is required")
|
||||||
@@ -592,7 +597,7 @@ func printSummary(stderr io.Writer, res *domain.RunResult) {
|
|||||||
|
|
||||||
func printUsage(w io.Writer) {
|
func printUsage(w io.Writer) {
|
||||||
fmt.Fprintln(w, "usage: scriptorium <run|render|serve> ...")
|
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, " 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.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.Fprintf(w, " serve: scriptorium serve [--config PATH] [--addr %s] [--prompt-dir DIR] [--profile-dir DIR] [--schema-dir DIR]\n", defaults.HTTPAddrDefault)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,11 +69,17 @@ func TestParseRunArgsRequiredFlags(t *testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected missing --prompt-dir error")
|
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"})
|
_, err = parseRunArgs([]string{"--config", configPath, "--prompt-dir", "./prompts", "--prompt", "p", "--input", "a=b"})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected missing --profile-dir error")
|
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"})
|
_, err = parseRunArgs([]string{"--config", configPath, "--prompt-dir", "./prompts", "--profile-dir", "./profiles", "--input", "a=b"})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -150,11 +156,17 @@ func TestParseServeArgsRequiredFlags(t *testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected missing --prompt-dir error")
|
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"})
|
_, err = parseServeArgs([]string{"--config", configPath, "--prompt-dir", "./prompts"})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected missing --profile-dir error")
|
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"})
|
cfg, err := parseServeArgs([]string{"--config", configPath, "--prompt-dir", "./prompts", "--profile-dir", "./profiles"})
|
||||||
if err != nil {
|
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) {
|
func TestDetermineExitCode(t *testing.T) {
|
||||||
if got := determineExitCode(errors.New("boom"), nil); got != ExitRuntimeError {
|
if got := determineExitCode(errors.New("boom"), nil); got != ExitRuntimeError {
|
||||||
t.Fatalf("expected runtime exit code, got %d", got)
|
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) {
|
func TestRenderCommandDefaultFormatTextIncludesPreparedDetailsAndNoSecrets(t *testing.T) {
|
||||||
const envName = "SCRIPTORIUM_RENDER_TEST_API_KEY"
|
const envName = "SCRIPTORIUM_RENDER_TEST_API_KEY"
|
||||||
const secret = "super-secret-render-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) {
|
func TestRenderCommandExplicitTextFormatWorks(t *testing.T) {
|
||||||
tmp := t.TempDir()
|
tmp := t.TempDir()
|
||||||
promptDir := filepath.Join(tmp, "prompts")
|
promptDir := filepath.Join(tmp, "prompts")
|
||||||
|
|||||||
Reference in New Issue
Block a user