commitBinding finalizes the bind: atomic write of the new workspace config, best-effort cleanup of stale keychain entries from the previous binding (if any), and a JSON success envelope. Cleanup runs only after the new config is durably written — if anything fails earlier, the old workspace stays us
(opts *BindOptions, appConfig *core.AppConfig, previousConfigBytes []byte, source, configPath string)
| 405 | // is durably written — if anything fails earlier, the old workspace stays |
| 406 | // usable. |
| 407 | func commitBinding(opts *BindOptions, appConfig *core.AppConfig, previousConfigBytes []byte, source, configPath string) error { |
| 408 | multi := &core.MultiAppConfig{Apps: []core.AppConfig{*appConfig}} |
| 409 | |
| 410 | if err := vfs.MkdirAll(core.GetConfigDir(), 0700); err != nil { |
| 411 | return errs.NewInternalError(errs.SubtypeFileIO, "failed to create workspace directory: %v", err).WithCause(err) |
| 412 | } |
| 413 | data, err := json.MarshalIndent(multi, "", " ") |
| 414 | if err != nil { |
| 415 | return errs.NewInternalError(errs.SubtypeStorage, "failed to marshal config: %v", err).WithCause(err) |
| 416 | } |
| 417 | if err := validate.AtomicWrite(configPath, append(data, '\n'), 0600); err != nil { |
| 418 | return errs.NewInternalError(errs.SubtypeStorage, "failed to write config %s: %v", configPath, err).WithCause(err) |
| 419 | } |
| 420 | |
| 421 | replaced := previousConfigBytes != nil |
| 422 | // uiMsg renders human-facing TUI text (stderr success banner). Follows |
| 423 | // opts.UILang — zh by default; picker can flip it to en. --lang does |
| 424 | // not influence the TUI language. |
| 425 | uiMsg := getBindMsg(opts.UILang) |
| 426 | display := sourceDisplayName(source) |
| 427 | |
| 428 | if replaced { |
| 429 | cleanupKeychainFromData(opts.Factory.Keychain, previousConfigBytes, appConfig) |
| 430 | } |
| 431 | |
| 432 | fmt.Fprintln(opts.Factory.IOStreams.ErrOut, |
| 433 | fmt.Sprintf(uiMsg.BindSuccessHeader, display)+"\n"+uiMsg.BindSuccessNotice) |
| 434 | |
| 435 | if opts.langExplicit && opts.Lang != "" { |
| 436 | fmt.Fprintln(opts.Factory.IOStreams.ErrOut, fmt.Sprintf(uiMsg.LangPreferenceSet, opts.Lang)) |
| 437 | } |
| 438 | |
| 439 | // TUI mode is a human sitting at a terminal; the BindSuccess notice on |
| 440 | // stderr is enough and a machine-readable JSON dump on stdout is just |
| 441 | // noise. Flag mode (Agent orchestration, scripts, piped output) still |
| 442 | // gets the full envelope for programmatic consumption. |
| 443 | if opts.IsTUI { |
| 444 | return nil |
| 445 | } |
| 446 | |
| 447 | envelope := map[string]interface{}{ |
| 448 | "ok": true, |
| 449 | "workspace": source, |
| 450 | "app_id": appConfig.AppId, |
| 451 | "config_path": configPath, |
| 452 | "replaced": replaced, |
| 453 | "identity": opts.Identity, |
| 454 | } |
| 455 | // JSON "message" follows the effective preference on disk (appConfig.Lang), |
| 456 | // not the raw --lang value: when --lang is omitted on re-bind, preferredLang |
| 457 | // has already inherited the prior preference into appConfig.Lang, and the |
| 458 | // message should respect that inherited choice. stderr above follows UILang. |
| 459 | prefMsg := getBindMsg(appConfig.Lang) |
| 460 | brand := brandDisplay(string(appConfig.Brand), appConfig.Lang) |
| 461 | switch opts.Identity { |
| 462 | case "bot-only": |
| 463 | envelope["message"] = fmt.Sprintf(prefMsg.MessageBotOnly, appConfig.AppId, display, brand) |
| 464 | case "user-default": |
no test coverage detected