diff --git a/cmd/launch/codex.go b/cmd/launch/codex.go index 31d587b77..22c3bb75c 100644 --- a/cmd/launch/codex.go +++ b/cmd/launch/codex.go @@ -215,6 +215,9 @@ func ensureCodexConfig(modelName string, models []LaunchModel) error { if err := os.MkdirAll(codexDir, 0o755); err != nil { return err } + if err := cleanupCodexLegacyProfileConfig(configPath); err != nil { + return err + } catalogPath := codexModelCatalogPathForConfig(configPath) if err := writeCodexModelCatalog(catalogPath, codexCatalogModel(modelName, models)); err != nil { @@ -261,6 +264,36 @@ func codexNamedProfileConfigPathForConfig(configPath, profileName string) string return filepath.Join(filepath.Dir(configPath), profileName+".config.toml") } +func cleanupCodexLegacyProfileConfig(configPath string) error { + content, err := os.ReadFile(configPath) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + text := string(content) + parsed, err := codexParseConfig(text) + if err != nil { + return err + } + + updated := text + if profile, ok := parsed.RootStringOK(codexRootProfileKey); ok && profile == codexProfileName { + updated = codexRemoveRootValue(updated, codexRootProfileKey) + } + if parsed.Exists("profiles", codexProfileName) { + updated = codexRemoveSection(updated, codexProfileHeader()) + } + if updated == text { + return nil + } + if err := codexValidateConfigText(updated); err != nil { + return err + } + return fileutil.WriteWithBackup(configPath, []byte(updated), "") +} + // writeCodexProfileConfig ensures ~/.codex/ollama-launch.config.toml selects // the Ollama provider and catalog for CLI launches without changing root config. func writeCodexProfileConfig(profilePath, model, modelCatalogPath string) error { diff --git a/cmd/launch/codex_test.go b/cmd/launch/codex_test.go index 2d162ac1a..9e4cf776d 100644 --- a/cmd/launch/codex_test.go +++ b/cmd/launch/codex_test.go @@ -312,6 +312,63 @@ func TestEnsureCodexConfig(t *testing.T) { t.Errorf("expected exactly one [model_providers.ollama-launch] section after two calls, got %d", strings.Count(content, "[model_providers.ollama-launch]")) } }) + + t.Run("cleans legacy root profile that conflicts with --profile", func(t *testing.T) { + tmpDir := t.TempDir() + setTestHome(t, tmpDir) + + configPath := filepath.Join(tmpDir, ".codex", "config.toml") + if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil { + t.Fatal(err) + } + existing := "" + + `profile = "ollama-launch"` + "\n" + + `model = "gpt-5.5"` + "\n" + + `model_provider = "openai"` + "\n\n" + + "[profiles.ollama-launch]\n" + + `model = "old-local"` + "\n" + + `model_provider = "ollama-launch"` + "\n\n" + + "[profiles.default]\n" + + `model = "gpt-5.5"` + "\n" + if err := os.WriteFile(configPath, []byte(existing), 0o644); err != nil { + t.Fatal(err) + } + + if err := ensureCodexConfig("llama3.2", launchModelsFromNames([]string{"llama3.2"})); err != nil { + t.Fatal(err) + } + + data, err := os.ReadFile(configPath) + if err != nil { + t.Fatal(err) + } + content := string(data) + if got, ok := codexRootStringValueOK(content, codexRootProfileKey); ok { + t.Fatalf("legacy root profile should be removed, got %q in:\n%s", got, content) + } + if strings.Contains(content, codexProfileHeader()) { + t.Fatalf("legacy profile table should be removed, got:\n%s", content) + } + for _, want := range []string{ + `model = "gpt-5.5"`, + `model_provider = "openai"`, + "[profiles.default]", + } { + if !strings.Contains(content, want) { + t.Fatalf("expected %q to be preserved in:\n%s", want, content) + } + } + + profilePath := filepath.Join(tmpDir, ".codex", "ollama-launch.config.toml") + profileData, err := os.ReadFile(profilePath) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(profileData), `model = "llama3.2"`) { + t.Fatalf("managed profile was not written with selected model:\n%s", profileData) + } + assertBackupContains(t, filepath.Join(fileutil.BackupDir(), "config.toml.*"), `profile = "ollama-launch"`) + }) } func TestCodexRestoreRemovesCLIProfileAndCatalogWithoutChangingUserRootConfig(t *testing.T) {