package cli import ( "fmt" "io" tea "github.com/charmbracelet/bubbletea" "git.dcglab.co.uk/steve/emcli/internal/crypto" "git.dcglab.co.uk/steve/emcli/internal/store" "git.dcglab.co.uk/steve/emcli/internal/tui" ) // runForm launches the bubbletea account form and returns the final model. // This is interactive terminal glue (not unit-tested); all logic it relies on // lives in the tui and store packages, which are tested. func runForm(initial tui.Fields, editing bool) (tui.AccountForm, error) { p := tea.NewProgram(tui.NewAccountForm(initial, editing)) m, err := p.Run() if err != nil { return tui.AccountForm{}, err } return m.(tui.AccountForm), nil } // addInteractive runs the form for a new account and persists it. func addInteractive(st *store.Store, initial tui.Fields, out, errOut io.Writer) int { form, err := runForm(initial, false) if err != nil { fmt.Fprintf(errOut, "form: %v\n", err) return 1 } if form.Cancelled() { fmt.Fprintln(out, "cancelled") return 1 } acc := form.Account() if _, err := st.AddAccount(acc); err != nil { fmt.Fprintf(errOut, "add account: %v\n", err) return 1 } fmt.Fprintf(out, "account %q added (%s)\n", acc.Name, acc.Mode) return 0 } // editInteractive runs the form prefilled from an existing account and saves it. func editInteractive(st *store.Store, name string, out, errOut io.Writer) int { acc, err := st.GetAccount(name) if err != nil { fmt.Fprintf(errOut, "edit: %v\n", err) return 1 } form, err := runForm(tui.FieldsFromAccount(acc), true) if err != nil { fmt.Fprintf(errOut, "form: %v\n", err) return 1 } if form.Cancelled() { fmt.Fprintln(out, "cancelled") return 1 } updated := form.Account() if !form.PasswordSet() { updated.Password = "" // blank ⇒ UpdateAccount keeps the existing password } if err := st.UpdateAccount(updated); err != nil { fmt.Fprintf(errOut, "edit: %v\n", err) return 1 } fmt.Fprintf(out, "account %q updated\n", name) return 0 } // runInit creates/opens the DB, writes both DEK wrap slots, and adds the first // account via the TUI form, seeding a default audit retention if unset. func runInit(args []string, out, errOut io.Writer) int { if len(args) > 0 && helpRequested(args[0]) { printCmdUsage(out, "init") return 0 } adminKey, err := crypto.AdminKeyFromEnv() if err != nil { fmt.Fprintf(errOut, "emcli: %v\n", err) return 1 } agentKey, err := crypto.AgentKeyFromEnv() if err != nil { fmt.Fprintf(errOut, "emcli: %v\n", err) return 1 } path, err := store.DefaultDBPath() if err != nil { fmt.Fprintf(errOut, "emcli: %v\n", err) return 1 } st, err := store.Open(path) if err != nil { fmt.Fprintf(errOut, "emcli: %v\n", err) return 1 } defer st.Close() if err := st.InitKeys(adminKey, agentKey); err != nil { fmt.Fprintf(errOut, "emcli: %v\n", err) return 1 } if _, err := st.GetSetting("audit_retention_days"); err != nil { _ = st.SetSetting("audit_retention_days", "90") } accs, _ := st.ListAccounts() if len(accs) > 0 { fmt.Fprintf(out, "emcli is already initialized (%d account(s)); adding another.\n", len(accs)) } else { fmt.Fprintln(out, "Initializing emcli — add your first account.") } return addInteractive(st, tui.Fields{}, out, errOut) }