From 6c2541c1ee99a9bb53d3fdb01a71580caf4d1b76 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sat, 29 Aug 2026 15:16:04 +0000 Subject: [PATCH] Unify inspection configuration handling --- internal/adapter/cli/inspect.go | 37 +++++++++++++++++++--------- internal/adapter/cli/run.go | 6 ++++- internal/adapter/cli/run_test.go | 42 +++++++++++++++++++++++++------- 3 files changed, 63 insertions(+), 22 deletions(-) diff --git a/internal/adapter/cli/inspect.go b/internal/adapter/cli/inspect.go index cecd1a0..d574eb6 100644 --- a/internal/adapter/cli/inspect.go +++ b/internal/adapter/cli/inspect.go @@ -6,23 +6,36 @@ import ( "flag" "fmt" "io" + "io/fs" "path/filepath" "strings" + "gitea.maximumdirect.net/eric/promptkit" appconfig "gitea.maximumdirect.net/eric/scriptorium/internal/config" appformat "gitea.maximumdirect.net/eric/scriptorium/internal/format" ) type promptInspectionConfig struct { configPath, promptDir, promptID, promptVersion, outputPath string + configExplicit bool outputFormat appformat.OutputFormat } type profileInspectionConfig struct { configPath, profileDir, profileID, outputPath string + configExplicit bool outputFormat appformat.OutputFormat } +// emptyPromptDefinitionFS satisfies Promptkit's engine-level prompt-source +// requirement without exposing the caller's working directory. Profile +// inspection never reads this source. +type emptyPromptDefinitionFS struct{} + +func (emptyPromptDefinitionFS) Open(name string) (fs.File, error) { + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} +} + func inspectCommand(args []string, stdout, stderr io.Writer) int { if len(args) == 0 { fmt.Fprintln(stderr, "inspect parse error: inspection mode is required") @@ -78,7 +91,7 @@ func inspectProfileCommand(args []string, stdout, stderr io.Writer) int { fmt.Fprintf(stderr, "inspect error: %v\n", err) return ExitRuntimeError } - engine, err := newEngine(settings) + engine, err := newEngine(settings, promptkit.WithPromptFS(emptyPromptDefinitionFS{}, ".")) if err != nil { fmt.Fprintf(stderr, "engine error: %v\n", err) return ExitRuntimeError @@ -113,6 +126,7 @@ func parseProfileInspectionArgs(args []string) (*profileInspectionConfig, error) if err := fs.Parse(args); err != nil { return nil, err } + cfg.configExplicit = flagWasSet(fs, "config") if fs.NArg() > 0 { return nil, fmt.Errorf("unexpected positional args: %v", fs.Args()) } @@ -131,14 +145,13 @@ func parseProfileInspectionArgs(args []string) (*profileInspectionConfig, error) } func resolveAppSettingsForProfileInspection(cfg *profileInspectionConfig) (engineSettings, error) { - settings, err := appconfig.LoadConfig(cfg.configPath, cfg.configPath != "") + settings, err := resolveAppSettingsWithConfigPresence(cfg.configPath, cfg.configExplicit, appconfig.CLIOverrides{ + ProfileDir: cfg.profileDir, + }) if err != nil { - return engineSettings{}, fmt.Errorf("application config error: %w", err) + return engineSettings{}, err } - if strings.TrimSpace(cfg.profileDir) != "" { - settings.ProfileDir = filepath.Clean(cfg.profileDir) - } - return engineSettings{promptDir: ".", profileDir: settings.ProfileDir, backends: settings.Backends}, nil + return engineSettings{profileDir: settings.ProfileDir, backends: settings.Backends}, nil } func parsePromptInspectionArgs(args []string) (*promptInspectionConfig, error) { @@ -155,6 +168,7 @@ func parsePromptInspectionArgs(args []string) (*promptInspectionConfig, error) { if err := fs.Parse(args); err != nil { return nil, err } + cfg.configExplicit = flagWasSet(fs, "config") if fs.NArg() > 0 { return nil, fmt.Errorf("unexpected positional args: %v", fs.Args()) } @@ -173,12 +187,11 @@ func parsePromptInspectionArgs(args []string) (*promptInspectionConfig, error) { } func resolveAppSettingsForPromptInspection(cfg *promptInspectionConfig) (engineSettings, error) { - settings, err := appconfig.LoadConfig(cfg.configPath, cfg.configPath != "") + settings, err := resolveAppSettingsWithConfigPresence(cfg.configPath, cfg.configExplicit, appconfig.CLIOverrides{ + PromptDir: cfg.promptDir, + }) if err != nil { - return engineSettings{}, fmt.Errorf("application config error: %w", err) - } - if strings.TrimSpace(cfg.promptDir) != "" { - settings.PromptDir = filepath.Clean(cfg.promptDir) + return engineSettings{}, err } if strings.TrimSpace(settings.PromptDir) == "" { return engineSettings{}, errors.New(errPromptDirRequired) diff --git a/internal/adapter/cli/run.go b/internal/adapter/cli/run.go index cdf78ea..e232b30 100644 --- a/internal/adapter/cli/run.go +++ b/internal/adapter/cli/run.go @@ -508,7 +508,11 @@ func registerConfigPathFlag(fs *flag.FlagSet, target *string) { } func resolveAppSettings(fs *flag.FlagSet, configPath string, overrides appconfig.CLIOverrides) (appconfig.AppSettings, error) { - settings, err := appconfig.LoadConfig(configPath, flagWasSet(fs, "config")) + return resolveAppSettingsWithConfigPresence(configPath, flagWasSet(fs, "config"), overrides) +} + +func resolveAppSettingsWithConfigPresence(configPath string, configExplicit bool, overrides appconfig.CLIOverrides) (appconfig.AppSettings, error) { + settings, err := appconfig.LoadConfig(configPath, configExplicit) if err != nil { return appconfig.AppSettings{}, fmt.Errorf("application config error: %w", err) } diff --git a/internal/adapter/cli/run_test.go b/internal/adapter/cli/run_test.go index 73180ec..944bc45 100644 --- a/internal/adapter/cli/run_test.go +++ b/internal/adapter/cli/run_test.go @@ -1183,6 +1183,24 @@ output: } } +func TestInspectionParsersPreserveExplicitEmptyConfigPath(t *testing.T) { + promptConfig, err := parsePromptInspectionArgs([]string{"--config=", "--prompt", "fixture"}) + if err != nil { + t.Fatalf("parse prompt inspection: %v", err) + } + if promptConfig.configPath != "" || !promptConfig.configExplicit { + t.Fatalf("expected explicit empty prompt config path, got %+v", promptConfig) + } + + profileConfig, err := parseProfileInspectionArgs([]string{"--config=", "--profile", "fixture"}) + if err != nil { + t.Fatalf("parse profile inspection: %v", err) + } + if profileConfig.configPath != "" || !profileConfig.configExplicit { + t.Fatalf("expected explicit empty profile config path, got %+v", profileConfig) + } +} + func TestPromptkitV09DefinitionsRenderThroughCLI(t *testing.T) { fixtureRoot := promptkitV09FixtureRoot(t) configPath := writePromptkitV09Config(t, fixtureRoot, true) @@ -1283,27 +1301,22 @@ func TestPromptkitV09ProfileInspectionResolvesSupportedTargets(t *testing.T) { t.Setenv("FIXTURE_PROFILE_API_KEY", secret) fixtureRoot := promptkitV09FixtureRoot(t) - configPath := writePromptkitV09Config(t, fixtureRoot, false) - profileDir := filepath.Join(fixtureRoot, "profiles") + configPath := writePromptkitV09Config(t, fixtureRoot, true) tests := []struct { name string profileID string - profileDir string wantBackend string wantModel string wantAPIKeyEnv string }{ - {name: "inherited custom backend", profileID: "custom-derived", profileDir: profileDir, wantBackend: "fixture-custom", wantModel: "fixture-derived-model", wantAPIKeyEnv: "FIXTURE_PROFILE_API_KEY"}, - {name: "endpoint only", profileID: "endpoint-only", profileDir: profileDir, wantModel: "fixture-endpoint-model"}, + {name: "inherited custom backend", profileID: "custom-derived", wantBackend: "fixture-custom", wantModel: "fixture-derived-model", wantAPIKeyEnv: "FIXTURE_PROFILE_API_KEY"}, + {name: "endpoint only", profileID: "endpoint-only", wantModel: "fixture-endpoint-model"}, {name: "built in", profileID: "deepseek-4-flash", wantBackend: "openrouter", wantModel: "deepseek/deepseek-v4-flash", wantAPIKeyEnv: "OPENROUTER_API_KEY"}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { args := []string{"profile", "--config", configPath, "--profile", tc.profileID, "--format", "json"} - if tc.profileDir != "" { - args = append(args, "--profile-dir", tc.profileDir) - } code, stdout, stderr := runCLICommand(t, inspectCommand, args) if code != ExitOK { t.Fatalf("expected ExitOK, got %d stderr=%q", code, stderr) @@ -1326,11 +1339,22 @@ func TestPromptkitV09ProfileInspectionResolvesSupportedTargets(t *testing.T) { } }) } + + configWithoutPromptDir := writePromptkitV09Config(t, fixtureRoot, false) + code, _, stderr := runCLICommand(t, inspectCommand, []string{ + "profile", + "--config", configWithoutPromptDir, + "--profile-dir", filepath.Join(fixtureRoot, "profiles"), + "--profile", "custom-derived", + }) + if code != ExitOK { + t.Fatalf("profile inspection unexpectedly required a prompt directory: %q", stderr) + } } func TestProfileInspectionHonorsDirectoryPrecedenceOutputAndFailures(t *testing.T) { fixtureRoot := promptkitV09FixtureRoot(t) - configPath := writePromptkitV09Config(t, fixtureRoot, false) + configPath := writePromptkitV09Config(t, fixtureRoot, true) overrideDir := t.TempDir() writeProfileFile(t, overrideDir, "custom-derived", "http://127.0.0.1:9000/v1", "override-model") outPath := filepath.Join(t.TempDir(), "inspection.json")