package cli import ( "bufio" "bytes" "context" "errors" "fmt" "io" "os" "sort" "strings" "time" "reasonix/internal/config" "reasonix/internal/i18n" ) type providerSetupSession struct { cfg *config.Config originalProviders map[string]config.ProviderEntry originalDefault string pendingCredentials map[string]string removed map[string]bool accessDeclared bool projectScoped bool declaredProviders []string operations []providerSetupOperation } const setupManagerContinue = 2 type providerSetupOperationKind uint8 const ( setupOpProvider providerSetupOperationKind = iota setupOpDefaultModel setupOpLanguage setupOpMaterializeAccess setupOpAccessMembership ) type providerSetupOperation struct { kind providerSetupOperationKind providerName string beforeProvider *config.ProviderEntry afterProvider *config.ProviderEntry beforeString string afterString string accessName string projectScoped bool beforeBool bool afterBool bool } type providerSetupConflictError struct { field string } type providerSetupFileSnapshot struct { exists bool body []byte } func (e *providerSetupConflictError) Error() string { return e.field } func providerSetupEntryPtr(entry config.ProviderEntry) *config.ProviderEntry { copy := config.ProviderEntryConfigSnapshot(entry) return © } func readProviderSetupFileSnapshot(path string) (providerSetupFileSnapshot, error) { body, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return providerSetupFileSnapshot{}, nil } return providerSetupFileSnapshot{}, err } return providerSetupFileSnapshot{exists: true, body: body}, nil } func providerSetupFileSnapshotEqual(a, b providerSetupFileSnapshot) bool { return a.exists == b.exists && bytes.Equal(a.body, b.body) } func newProviderSetupSession(cfg *config.Config) *providerSetupSession { s := &providerSetupSession{ cfg: cfg, originalProviders: make(map[string]config.ProviderEntry, len(cfg.Providers)), originalDefault: cfg.DefaultModel, pendingCredentials: map[string]string{}, removed: map[string]bool{}, } for _, p := range cfg.Providers { s.originalProviders[p.Name] = p } return s } func newProviderSetupSessionForPath(cfg *config.Config, path string) *providerSetupSession { s := newProviderSetupSession(cfg) s.projectScoped = !config.IsUserConfigPath(path) declarations, err := config.InspectConfigFileDeclarations(path) if err != nil { // LoadForEdit already reports malformed/unreadable config and falls back; // keep the conservative policy here so setup never enables hidden siblings. s.accessDeclared = true return s } s.accessDeclared = declarations.DesktopProviderAccessDeclared s.declaredProviders = declarations.ProviderNames return s } func (s *providerSetupSession) recordProviderMutation(name string, before, after *config.ProviderEntry) { s.operations = append(s.operations, providerSetupOperation{ kind: setupOpProvider, providerName: name, beforeProvider: before, afterProvider: after, }) } func (s *providerSetupSession) setLanguage(language string) { if s.cfg.Language == language { return } s.operations = append(s.operations, providerSetupOperation{ kind: setupOpLanguage, beforeString: s.cfg.Language, afterString: language, }) s.cfg.Language = language } func (s *providerSetupSession) applyDeepSeekOfficialDefaultPricing() { before := make(map[string]config.ProviderEntry, len(s.cfg.Providers)) for _, provider := range s.cfg.Providers { before[provider.Name] = provider } s.cfg.ApplyDeepSeekOfficialDefaultPricing() for i := range s.cfg.Providers { after := s.cfg.Providers[i] previous, existed := before[after.Name] if existed && config.ProviderEntriesConfigEqual(previous, after) { delete(before, after.Name) continue } var previousPtr *config.ProviderEntry if existed { previousPtr = providerSetupEntryPtr(previous) } s.recordProviderMutation(after.Name, previousPtr, providerSetupEntryPtr(after)) delete(before, after.Name) } for name, previous := range before { s.recordProviderMutation(name, providerSetupEntryPtr(previous), nil) } } func (s *providerSetupSession) resetProviderSummaryBaseline() { s.originalProviders = make(map[string]config.ProviderEntry, len(s.cfg.Providers)) for _, provider := range s.cfg.Providers { s.originalProviders[provider.Name] = provider } } func (s *providerSetupSession) upsert(entries []config.ProviderEntry) error { for _, entry := range entries { var before *config.ProviderEntry if current, ok := s.cfg.Provider(entry.Name); ok { before = providerSetupEntryPtr(*current) } if err := s.cfg.UpsertProvider(entry); err != nil { return err } current, _ := s.cfg.Provider(entry.Name) if before == nil || !config.ProviderEntriesConfigEqual(*before, *current) { s.recordProviderMutation(entry.Name, before, providerSetupEntryPtr(*current)) } delete(s.removed, entry.Name) s.repairDanglingDefaultFor(*current) } return nil } // repairDanglingDefaultFor re-points default_model at the provider's own default // when an edit or model refresh dropped the exact model the ref named, mirroring // the repair RemoveProvider performs on removal. func (s *providerSetupSession) repairDanglingDefaultFor(p config.ProviderEntry) { if !config.ModelRefsProvider(s.cfg.DefaultModel, p.Name) || len(p.ModelList()) == 0 { return } if _, ok := s.cfg.ResolveModel(s.cfg.DefaultModel); ok { return } if err := s.setDefaultModel(p.Name); err != nil { fmt.Fprintln(os.Stderr, err) } } func (s *providerSetupSession) add(entries []config.ProviderEntry) error { seen := make(map[string]bool, len(s.cfg.Providers)+len(entries)) for _, provider := range s.cfg.Providers { seen[provider.Name] = true } for _, entry := range entries { if seen[entry.Name] { return fmt.Errorf(i18n.M.SetupProviderExistsFmt, entry.Name) } seen[entry.Name] = true } return s.upsert(entries) } func (s *providerSetupSession) remove(name string) error { current, ok := s.cfg.Provider(name) if !ok { return fmt.Errorf("remove provider: no provider %q", name) } before := providerSetupEntryPtr(*current) if err := s.cfg.RemoveProvider(name); err != nil { return err } s.recordProviderMutation(name, before, nil) s.removeProviderAccess(name) if _, existed := s.originalProviders[name]; existed { s.removed[name] = true } return nil } func (s *providerSetupSession) addProviderAccess(entries []config.ProviderEntry) { if len(entries) == 0 { return } before := append([]string(nil), s.cfg.Desktop.ProviderAccess...) // Preserve the legacy "undeclared means infer all configured providers" // behavior before turning provider_access into an explicit list. Project // setup only seeds providers declared by that project; cfg also contains // built-in defaults, which must not override the user's global access policy. if !s.accessDeclared && len(s.cfg.Desktop.ProviderAccess) == 0 { if s.projectScoped { for _, name := range s.declaredProviders { provider, ok := s.cfg.Provider(name) if ok && provider.Configured() && len(provider.ModelList()) > 0 { s.cfg.Desktop.ProviderAccess = append(s.cfg.Desktop.ProviderAccess, name) } } } else { config.NormalizeLegacyDesktopProviderAccess(s.cfg) } s.accessDeclared = true s.operations = append(s.operations, providerSetupOperation{ kind: setupOpMaterializeAccess, projectScoped: s.projectScoped, }) before = append([]string(nil), s.cfg.Desktop.ProviderAccess...) } seen := make(map[string]bool, len(s.cfg.Desktop.ProviderAccess)+len(entries)) for _, name := range s.cfg.Desktop.ProviderAccess { name = strings.TrimSpace(name) if name != "" { seen[name] = true } } for _, entry := range entries { name := strings.TrimSpace(entry.Name) if name == "" || seen[name] { continue } s.cfg.Desktop.ProviderAccess = append(s.cfg.Desktop.ProviderAccess, name) seen[name] = true } s.accessDeclared = true s.recordAccessTransition(before) } func (s *providerSetupSession) removeProviderAccess(name string) { name = strings.TrimSpace(name) if name == "" || len(s.cfg.Desktop.ProviderAccess) == 0 { return } before := append([]string(nil), s.cfg.Desktop.ProviderAccess...) out := s.cfg.Desktop.ProviderAccess[:0] for _, current := range s.cfg.Desktop.ProviderAccess { if strings.TrimSpace(current) != name { out = append(out, current) } } s.cfg.Desktop.ProviderAccess = out s.recordAccessTransition(before) } func (s *providerSetupSession) recordAccessTransition(before []string) { beforeSet := make(map[string]bool, len(before)) afterSet := make(map[string]bool, len(s.cfg.Desktop.ProviderAccess)) var order []string seen := map[string]bool{} for _, names := range [][]string{before, s.cfg.Desktop.ProviderAccess} { for _, name := range names { name = strings.TrimSpace(name) if name == "" { continue } if !seen[name] { seen[name] = true order = append(order, name) } } } for _, name := range before { name = strings.TrimSpace(name) if name != "" { beforeSet[name] = true } } for _, name := range s.cfg.Desktop.ProviderAccess { name = strings.TrimSpace(name) if name != "" { afterSet[name] = true } } for _, name := range order { if beforeSet[name] == afterSet[name] { continue } s.operations = append(s.operations, providerSetupOperation{ kind: setupOpAccessMembership, accessName: name, beforeBool: beforeSet[name], afterBool: afterSet[name], }) } } func (s *providerSetupSession) setCredential(key, value string) error { key = strings.TrimSpace(key) if !config.IsValidCredentialKey(key) { return fmt.Errorf("invalid API key variable name %q", key) } if strings.ContainsAny(value, "\r\n") { return fmt.Errorf("API key for %s contains a newline", key) } s.pendingCredentials[key] = value return nil } func (s *providerSetupSession) setDefaultModel(model string) error { before := s.cfg.DefaultModel if err := s.cfg.SetDefaultModel(model); err != nil { return err } if before != s.cfg.DefaultModel { s.operations = append(s.operations, providerSetupOperation{ kind: setupOpDefaultModel, beforeString: before, afterString: s.cfg.DefaultModel, }) } return nil } // providerUsable reports whether the provider would be selectable once this // session saves: it lists models and either needs no key or has one resolvable // from the credential store or staged in this session. func (s *providerSetupSession) providerUsable(p *config.ProviderEntry) bool { if p == nil || len(p.ModelList()) == 0 { return false } return p.Configured() || s.pendingCredentials[p.APIKeyEnv] != "" } // defaultModelUsable reports whether default_model resolves to a provider the // user could actually run once this session saves. func (s *providerSetupSession) defaultModelUsable() bool { entry, ok := s.cfg.ResolveModel(s.cfg.DefaultModel) return ok && s.providerUsable(entry) } // promoteDefaultToNewProviders keeps the wizard's first-run contract: when the // current default_model cannot run (unresolvable, or its key is neither stored // nor staged), point it at the first usable provider the user just added, so a // first run that only configures a custom provider boots on that provider // instead of failing on the built-in default's missing key. A usable default is // never hijacked. func (s *providerSetupSession) promoteDefaultToNewProviders(entries []config.ProviderEntry) { if s.defaultModelUsable() { return } for _, entry := range entries { current, ok := s.cfg.Provider(entry.Name) if !ok || !s.providerUsable(current) { continue } if err := s.setDefaultModel(current.Name); err == nil { return } } } func (s *providerSetupSession) credentialLines() []string { keys := make([]string, 0, len(s.pendingCredentials)) for key := range s.pendingCredentials { keys = append(keys, key) } sort.Strings(keys) lines := make([]string, 0, len(keys)) for _, key := range keys { lines = append(lines, key+"="+s.pendingCredentials[key]) } return lines } func (s *providerSetupSession) summary() []string { var added, edited []string for _, p := range s.cfg.Providers { old, existed := s.originalProviders[p.Name] switch { case !existed: added = append(added, p.Name) case !providerSetupEqual(old, p): edited = append(edited, p.Name) } } var out []string if len(added) > 0 { out = append(out, fmt.Sprintf(i18n.M.SetupSummaryAddedFmt, strings.Join(added, ", "))) } if len(edited) > 0 { out = append(out, fmt.Sprintf(i18n.M.SetupSummaryEditedFmt, strings.Join(edited, ", "))) } if len(s.removed) > 0 { names := make([]string, 0, len(s.removed)) for name := range s.removed { names = append(names, name) } sort.Strings(names) out = append(out, fmt.Sprintf(i18n.M.SetupSummaryRemovedFmt, strings.Join(names, ", "))) } if s.cfg.DefaultModel != s.originalDefault { out = append(out, fmt.Sprintf(i18n.M.SetupSummaryDefaultFmt, s.cfg.DefaultModel)) } if len(s.pendingCredentials) > 0 { out = append(out, fmt.Sprintf(i18n.M.SetupSummaryKeysFmt, len(s.pendingCredentials))) } if len(out) == 0 { out = append(out, i18n.M.SetupSummaryNoChanges) } return out } func providerSetupEqual(a, b config.ProviderEntry) bool { // Render-level equality is unnecessary here: the manager only changes these // fields, while advanced provider fields are preserved by editing a copy. return a.Name == b.Name && a.Kind == b.Kind && a.BaseURL == b.BaseURL && a.Model == b.Model && strings.Join(a.Models, "\x00") == strings.Join(b.Models, "\x00") && a.Default == b.Default && a.APIKeyEnv == b.APIKeyEnv } func runProviderSetupManager(s *providerSetupSession, configPath, envPath string) int { cfg := s.cfg repaired, repairs := repairInvalidProviderKeyEnvs(cfg.Providers) for i := range repaired { if config.ProviderEntriesConfigEqual(cfg.Providers[i], repaired[i]) { continue } before := providerSetupEntryPtr(cfg.Providers[i]) cfg.Providers[i] = repaired[i] s.recordProviderMutation(repaired[i].Name, before, providerSetupEntryPtr(repaired[i])) } for _, repair := range repairs { fmt.Fprintf(os.Stderr, " %s\n", dim(fmt.Sprintf(i18n.M.RepairedAPIKeyEnvFmt, repair.provider, repair.old, repair.new))) } for { items := providerManagerItems(s) idx, err := selectOne(i18n.M.SetupManagerTitle, items) if err != nil { fmt.Fprintln(os.Stderr, "\n"+i18n.M.SetupCancelled) return 1 } providerCount := len(cfg.Providers) switch idx { case providerCount: if !addProviderToSession(s, false) { continue } case providerCount + 1: if !addProviderToSession(s, true) { continue } case providerCount + 2: rc := saveProviderSetupSession(s, configPath, envPath) if rc == setupManagerContinue { continue } return rc case providerCount + 3: fmt.Println(i18n.M.SetupCancelled) return 1 default: manageProvider(s, idx) } } } func providerManagerItems(s *providerSetupSession) []menuItem { cfg := s.cfg items := make([]menuItem, 0, len(cfg.Providers)+4) for _, p := range cfg.Providers { models := p.ModelList() keyStatus := i18n.M.SetupKeyMissing if p.APIKeyEnv == "" || config.CredentialIsSet(p.APIKeyEnv) || s.pendingCredentials[p.APIKeyEnv] != "" { keyStatus = i18n.M.SetupKeySet } desc := fmt.Sprintf("%s · %d %s · %s", p.Kind, len(models), i18n.M.SetupModelsUnit, keyStatus) if cfg.DefaultModel == p.Name || config.ModelRefsProvider(cfg.DefaultModel, p.Name) { desc += " · " + i18n.M.SetupDefaultBadge } items = append(items, menuItem{name: p.Name, desc: desc}) } return append(items, menuItem{name: i18n.M.SetupAddOpenAI, desc: i18n.M.CustomProviderDesc}, menuItem{name: i18n.M.SetupAddAnthropic, desc: i18n.M.AnthropicProviderDesc}, menuItem{name: i18n.M.SetupSaveExit, desc: i18n.M.SetupSaveExitDesc}, menuItem{name: i18n.M.SetupCancel, desc: i18n.M.SetupCancelDesc}, ) } func addProviderToSession(s *providerSetupSession, anthropic bool) bool { var result providerPromptResult var err error if anthropic { result, err = promptAnthropicProvider() } else { result, err = promptCustomProvider() } if err != nil { if err != errCancelled { fmt.Fprintln(os.Stderr, err) } return false } for _, entry := range result.entries { if !confirmSharedCredential(s.cfg, entry, "") { return false } } if err := s.add(result.entries); err != nil { fmt.Fprintln(os.Stderr, err) return false } s.addProviderAccess(result.entries) for key, value := range result.credentials { if err := s.setCredential(key, value); err != nil { fmt.Fprintln(os.Stderr, err) return false } } // After the new keys are staged, so usability sees them. s.promoteDefaultToNewProviders(result.entries) return true } func manageProvider(s *providerSetupSession, providerIndex int) { if providerIndex < 0 || providerIndex >= len(s.cfg.Providers) { return } p := s.cfg.Providers[providerIndex] idx, err := selectOne(fmt.Sprintf(i18n.M.SetupProviderActionsFmt, p.Name), []menuItem{ {name: i18n.M.SetupEditProvider}, {name: i18n.M.SetupUpdateKey}, {name: i18n.M.SetupTestRefresh}, {name: i18n.M.SetupSetDefault}, {name: i18n.M.SetupRemoveProvider}, {name: i18n.M.SetupBack}, }) if err != nil || idx == 5 { return } switch idx { case 0: editProvider(s, p) case 1: updateProviderKey(s, p) case 2: testAndRefreshProvider(s, p) case 3: setDefaultProvider(s, p) case 4: removeProviderFromSession(s, p) } } func editProvider(s *providerSetupSession, current config.ProviderEntry) { in := bufio.NewScanner(os.Stdin) edited := current edited.BaseURL = ask(in, os.Stdout, i18n.M.CustomPromptBaseURL, current.BaseURL) models := ask(in, os.Stdout, i18n.M.SetupPromptModels, strings.Join(current.ModelList(), ",")) edited.Models = splitModels(models) if len(edited.Models) == 1 { edited.Model = edited.Models[0] } else { edited.Model = "" } if len(edited.Models) > 0 && !containsString(edited.Models, edited.Default) { edited.Default = edited.Models[0] } edited.APIKeyEnv = promptOptionalAPIKeyEnvName(in, os.Stdout, i18n.M.CustomPromptKeyEnv, current.APIKeyEnv) if !confirmSharedCredential(s.cfg, edited, current.Name) { return } if err := s.upsert([]config.ProviderEntry{edited}); err != nil { fmt.Fprintln(os.Stderr, err) } } func promptOptionalAPIKeyEnvName(in *bufio.Scanner, w io.Writer, label, def string) string { for { key := ask(in, w, label, def) if key == "" || config.IsValidCredentialKey(key) { return key } fmt.Fprintf(w, i18n.M.InvalidAPIKeyEnvFmt+"\n", key) } } func splitModels(raw string) []string { seen := map[string]bool{} var models []string for _, model := range strings.Split(raw, ",") { model = strings.TrimSpace(model) if model != "" && !seen[model] { seen[model] = true models = append(models, model) } } return models } func confirmSharedCredential(cfg *config.Config, candidate config.ProviderEntry, ignoreName string) bool { if candidate.APIKeyEnv == "" { return true } for _, p := range cfg.Providers { if p.Name == ignoreName || p.Name == candidate.Name || p.APIKeyEnv != candidate.APIKeyEnv || p.BaseURL == candidate.BaseURL { continue } in := bufio.NewScanner(os.Stdin) answer := ask(in, os.Stdout, fmt.Sprintf(i18n.M.SetupSharedKeyWarningFmt, candidate.APIKeyEnv, p.Name, p.BaseURL), "y/N") return answer == "y" || answer == "Y" } return true } func updateProviderKey(s *providerSetupSession, p config.ProviderEntry) { in := bufio.NewScanner(os.Stdin) keyEnvChanged := false if p.APIKeyEnv == "" { p.APIKeyEnv = promptAPIKeyEnvName(in, os.Stdout, i18n.M.CustomPromptKeyEnv, apiKeyEnvFromProviderName(p.Name)) keyEnvChanged = true } if !confirmSharedCredential(s.cfg, p, p.Name) { return } if keyEnvChanged { if err := s.upsert([]config.ProviderEntry{p}); err != nil { fmt.Fprintln(os.Stderr, err) return } } value := ask(in, os.Stdout, fmt.Sprintf(i18n.M.SetupPromptAPIKeyFmt, p.APIKeyEnv), "") if value == "" { return } if err := s.setCredential(p.APIKeyEnv, value); err != nil { fmt.Fprintln(os.Stderr, err) } } func testAndRefreshProvider(s *providerSetupSession, p config.ProviderEntry) { restore := temporarilySetCredential(p.APIKeyEnv, s.pendingCredentials[p.APIKeyEnv]) defer restore() p.ResolveAPIKeyFromProcessEnvForProbe() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() models, err := p.FetchModels(ctx) if err != nil { fmt.Fprintf(os.Stderr, i18n.M.FetchModelsFailedFmt+"\n", p.Name, err) return } if len(models) == 0 { fmt.Fprintln(os.Stderr, i18n.M.CustomFetchEmpty) return } items := make([]menuItem, len(models)) for i, model := range models { items[i] = menuItem{name: model} } idxs, err := selectMany(fmt.Sprintf(i18n.M.SelectModelsLabel, p.Name), items) if err != nil || len(idxs) == 0 { return } selected := make([]string, 0, len(idxs)) for _, idx := range idxs { selected = append(selected, models[idx]) } p.Models = selected p.Model = "" if !containsString(selected, p.Default) { p.Default = selected[0] } if err := s.upsert([]config.ProviderEntry{p}); err != nil { fmt.Fprintln(os.Stderr, err) return } fmt.Printf(" %s\n", green(fmt.Sprintf(i18n.M.FetchModelsSuccessFmt, len(models), p.Name))) } func temporarilySetCredential(key, value string) func() { if key == "" || value == "" { return func() {} } old, existed := os.LookupEnv(key) _ = os.Setenv(key, value) return func() { if existed { _ = os.Setenv(key, old) } else { _ = os.Unsetenv(key) } } } func setDefaultProvider(s *providerSetupSession, p config.ProviderEntry) { models := p.ModelList() if len(models) == 0 { return } items := make([]menuItem, len(models)) for i, model := range models { items[i] = menuItem{name: model} } idx, err := selectOne(i18n.M.SetupSelectDefaultModel, items) if err != nil { return } if err := s.setDefaultModel(p.Name + "/" + models[idx]); err != nil { fmt.Fprintln(os.Stderr, err) } } func removeProviderFromSession(s *providerSetupSession, p config.ProviderEntry) { in := bufio.NewScanner(os.Stdin) answer := ask(in, os.Stdout, fmt.Sprintf(i18n.M.SetupConfirmRemoveFmt, p.Name), "y/N") if answer != "y" && answer != "Y" { return } if err := s.remove(p.Name); err != nil { fmt.Fprintln(os.Stderr, err) } } func (s *providerSetupSession) replayOperations(cfg *config.Config, accessDeclared *bool, declaredProviders []string) error { for _, operation := range s.operations { switch operation.kind { case setupOpProvider: current, exists := cfg.Provider(operation.providerName) if operation.beforeProvider == nil { if exists { return &providerSetupConflictError{field: fmt.Sprintf("provider %q", operation.providerName)} } } else if !exists || !config.ProviderEntriesConfigEqual(*current, *operation.beforeProvider) { return &providerSetupConflictError{field: fmt.Sprintf("provider %q", operation.providerName)} } if operation.afterProvider == nil { if err := cfg.RemoveProvider(operation.providerName); err != nil { return fmt.Errorf("replay remove provider %q: %w", operation.providerName, err) } } else if err := cfg.UpsertProviderPreservingRuntime(*operation.afterProvider); err != nil { return fmt.Errorf("replay provider %q: %w", operation.providerName, err) } case setupOpDefaultModel: if cfg.DefaultModel != operation.beforeString { return &providerSetupConflictError{field: "default_model"} } if err := cfg.SetDefaultModel(operation.afterString); err != nil { return fmt.Errorf("replay default_model: %w", err) } case setupOpLanguage: if cfg.Language != operation.beforeString { return &providerSetupConflictError{field: "language"} } cfg.Language = operation.afterString case setupOpMaterializeAccess: if *accessDeclared { return &providerSetupConflictError{field: "desktop.provider_access"} } cfg.Desktop.ProviderAccess = nil if operation.projectScoped { for _, name := range declaredProviders { provider, ok := cfg.Provider(name) if ok && provider.Configured() && len(provider.ModelList()) > 0 { cfg.Desktop.ProviderAccess = append(cfg.Desktop.ProviderAccess, name) } } } else { config.NormalizeLegacyDesktopProviderAccess(cfg) } *accessDeclared = true if cfg.Desktop.ProviderAccess == nil { cfg.Desktop.ProviderAccess = []string{} } case setupOpAccessMembership: current := providerSetupAccessContains(cfg.Desktop.ProviderAccess, operation.accessName) if current != operation.beforeBool { return &providerSetupConflictError{field: fmt.Sprintf("desktop.provider_access[%q]", operation.accessName)} } if operation.afterBool { cfg.Desktop.ProviderAccess = append(cfg.Desktop.ProviderAccess, operation.accessName) } else { out := cfg.Desktop.ProviderAccess[:0] for _, name := range cfg.Desktop.ProviderAccess { if strings.TrimSpace(name) != operation.accessName { out = append(out, name) } } cfg.Desktop.ProviderAccess = out } default: return fmt.Errorf("unknown provider setup operation %d", operation.kind) } } return nil } func providerSetupAccessContains(names []string, want string) bool { want = strings.TrimSpace(want) for _, name := range names { if strings.TrimSpace(name) == want { return true } } return false } func commitProviderSetupSession(s *providerSetupSession, configPath string) (bool, error) { if len(s.operations) == 0 { return false, nil } unlock := func() {} if config.IsUserConfigPath(configPath) { unlock = config.LockUserConfigEdits() } defer unlock() before, err := readProviderSetupFileSnapshot(configPath) if err != nil { return false, err } declarations, err := config.InspectConfigFileDeclarations(configPath) if err != nil { return false, err } fresh, err := config.LoadForEditReadOnlyStrict(configPath) if err != nil { return false, err } accessDeclared := declarations.DesktopProviderAccessDeclared if err := s.replayOperations(fresh, &accessDeclared, declarations.ProviderNames); err != nil { return false, err } current, err := readProviderSetupFileSnapshot(configPath) if err != nil { return false, err } if !providerSetupFileSnapshotEqual(before, current) { return false, &providerSetupConflictError{field: "configuration file"} } if err := fresh.SaveTo(configPath); err != nil { return false, err } return true, nil } func saveProviderSetupSession(s *providerSetupSession, configPath, envPath string) int { fmt.Println() fmt.Println(i18n.M.SetupSummaryTitle) for _, line := range s.summary() { fmt.Println(" " + line) } in := bufio.NewScanner(os.Stdin) answer := ask(in, os.Stdout, i18n.M.SetupConfirmSave, "Y/n") if answer == "n" || answer == "N" { return setupManagerContinue } configWritten, err := commitProviderSetupSession(s, configPath) if err != nil { var conflict *providerSetupConflictError if errors.As(err, &conflict) { fmt.Fprintf(os.Stderr, i18n.M.SetupConcurrentChangeFmt+"\n", conflict.field) } else { fmt.Fprintln(os.Stderr, i18n.M.WriteConfigErr, err) } return 1 } if configWritten { fmt.Printf("\n%s %s\n", green("✓"), fmt.Sprintf(i18n.M.WroteFileFmt, displayPath(configPath))) } if lines := s.credentialLines(); len(lines) > 0 { target, err := config.StoreCredentialLines(lines) if err != nil { fmt.Fprintln(os.Stderr, i18n.M.WriteEnvErr, err) return 1 } if target == "" { target = envPath } fmt.Printf("%s %s\n", green("✓"), fmt.Sprintf(i18n.M.WroteFileFmt, displayPath(target))) } fmt.Printf("\n%s %s\n", accent("◆"), i18n.M.SetupComplete) return 0 }