diff --git a/internal/cmd/credentialcmd/credentialcmd.go b/internal/cmd/credentialcmd/credentialcmd.go index 5f93e3b..cded1ee 100644 --- a/internal/cmd/credentialcmd/credentialcmd.go +++ b/internal/cmd/credentialcmd/credentialcmd.go @@ -1681,19 +1681,23 @@ func (p huhInitFinalizePrompter) ChooseFinalizeAction(prompt initFinalizePrompt) func initFinalizeDescription(prompt initFinalizePrompt) string { lines := []string{"Review readiness before committing staged config and credential changes."} for _, profile := range prompt.Profiles { - status := "ready" - if !profile.Ready { - status = "needs follow-up" - } - line := fmt.Sprintf("- %s: %s", profile.ProfileName, status) - if len(profile.Notes) > 0 { - line += " (" + strings.Join(profile.Notes, "; ") + ")" - } - lines = append(lines, line) + lines = append(lines, initProfileReadinessLine(profile)) } return strings.Join(lines, "\n") } +func initProfileReadinessLine(profile initProfileReadiness) string { + status := "ready" + if !profile.Ready { + status = "needs follow-up" + } + line := fmt.Sprintf("- %s: %s", profile.ProfileName, status) + if len(profile.Notes) > 0 { + line += " (" + strings.Join(profile.Notes, "; ") + ")" + } + return line +} + func (p huhInitLLMRuntimePrompter) EditLLMRuntime(prompt initLLMRuntimePrompt) (initDraft, error) { if p.inventoryRunner == nil { return p.editLLMRuntimeLinear(prompt) @@ -6138,16 +6142,54 @@ func initCredentialReadinessNote(entry initCredentialPlanEntry) string { case initCredentialPlanStateKeepExisting, initCredentialPlanStateWrite, initCredentialPlanStateClearRef: return "" case initCredentialPlanStateDefer: - return label + " deferred" + return label + " deferred" + initCredentialScopedKeySummary(entry) case initCredentialPlanStateOverwriteRef, initCredentialPlanStateMissingRequired: if len(entry.MissingRequiredKeys) == 0 { - return label + " needs setup" + return label + " needs setup" + initCredentialScopedKeySummary(entry) + } + if initCredentialShouldSummarizeKeys(entry) { + return fmt.Sprintf("%s missing required %s%s", label, strings.Join(entry.MissingRequiredKeys, ", "), initCredentialOptionalKeySummary(entry)) } return fmt.Sprintf("%s missing %s", label, strings.Join(entry.MissingRequiredKeys, ", ")) } return "" } +func initCredentialScopedKeySummary(entry initCredentialPlanEntry) string { + if !initCredentialShouldSummarizeKeys(entry) { + return "" + } + return initCredentialKeySummary(entry) +} + +func initCredentialShouldSummarizeKeys(entry initCredentialPlanEntry) bool { + return len(entry.KeySpecs) > 1 +} + +func initCredentialKeySummary(entry initCredentialPlanEntry) string { + required := initCredentialRequiredKeys(entry) + optional := initCredentialOptionalKeys(entry) + if len(required) == 0 && len(optional) == 0 { + return "" + } + parts := []string{} + if len(required) > 0 { + parts = append(parts, "required: "+strings.Join(required, ", ")) + } + if len(optional) > 0 { + parts = append(parts, "optional: "+strings.Join(optional, ", ")) + } + return " (" + strings.Join(parts, "; ") + ")" +} + +func initCredentialOptionalKeySummary(entry initCredentialPlanEntry) string { + optional := initCredentialOptionalKeys(entry) + if len(optional) == 0 { + return "" + } + return " (optional: " + strings.Join(optional, ", ") + ")" +} + func applyInteractiveInitSessionPlan(opts *root.Options, deps initDeps, plan initSessionPlan) error { if err := config.Validate(plan.cfg); err != nil { if errors.Is(err, config.ErrInvalid) || errors.Is(err, config.ErrProfileNotFound) { @@ -6192,11 +6234,7 @@ func applyInteractiveInitSessionPlan(opts *root.Options, deps initDeps, plan ini return err } for _, readiness := range buildInteractiveInitProfileReadiness(plan) { - status := "ready" - if !readiness.Ready { - status = "needs follow-up" - } - if _, err := fmt.Fprintf(opts.Stdout, "- %s: %s\n", readiness.ProfileName, status); err != nil { + if _, err := fmt.Fprintln(opts.Stdout, initProfileReadinessLine(readiness)); err != nil { return err } } @@ -6426,10 +6464,7 @@ func applyInitPlan(opts *root.Options, flags initOptions, deps initDeps, plan in func writeInitCredentialPlanHints(w io.Writer, backendArg string, entry initCredentialPlanEntry) error { keys := entry.MissingRequiredKeys if len(keys) == 0 { - keys = make([]string, 0, len(entry.KeySpecs)) - for _, spec := range entry.KeySpecs { - keys = append(keys, spec.Key) - } + keys = initCredentialRequiredKeys(entry) } hintPrefix := "Next" if hintLabel := initSecretsProfileHintLabel(entry.SecretsProfile); hintLabel != "" { @@ -6443,6 +6478,28 @@ func writeInitCredentialPlanHints(w io.Writer, backendArg string, entry initCred return nil } +func initCredentialRequiredKeys(entry initCredentialPlanEntry) []string { + keys := make([]string, 0, len(entry.KeySpecs)) + for _, spec := range entry.KeySpecs { + if !spec.Required { + continue + } + keys = append(keys, spec.Key) + } + return keys +} + +func initCredentialOptionalKeys(entry initCredentialPlanEntry) []string { + keys := make([]string, 0, len(entry.KeySpecs)) + for _, spec := range entry.KeySpecs { + if spec.Required { + continue + } + keys = append(keys, spec.Key) + } + return keys +} + func projectInitPlannedWriteKeys(writes map[string]map[string]string) map[string][]string { projected := make(map[string][]string, len(writes)) for ref, bundle := range writes { diff --git a/internal/cmd/credentialcmd/credentialcmd_test.go b/internal/cmd/credentialcmd/credentialcmd_test.go index 7ce1c72..3c52d2a 100644 --- a/internal/cmd/credentialcmd/credentialcmd_test.go +++ b/internal/cmd/credentialcmd/credentialcmd_test.go @@ -565,11 +565,14 @@ func TestInitNonInteractiveWritesGitHubAppReviewerConfigOnly(t *testing.T) { if reviewer == nil || reviewer.AuthMode != config.GitAuthModeGitHubApp || reviewer.CredentialRef != "codereview/default-reviewer" { t.Fatalf("reviewer credentials = %#v, want github_app codereview/default-reviewer", reviewer) } - for _, key := range []string{credentials.GitHubAppIDKey, credentials.GitHubAppPrivateKeyKey, credentials.GitHubAppInstallationIDKey} { + for _, key := range []string{credentials.GitHubAppIDKey, credentials.GitHubAppPrivateKeyKey} { if !strings.Contains(errOut.String(), "--key "+key+" --stdin") { t.Fatalf("stderr = %q, want setup hint for %s", errOut.String(), key) } } + if strings.Contains(errOut.String(), "--key "+credentials.GitHubAppInstallationIDKey+" --stdin") { + t.Fatalf("stderr = %q, want optional installation id omitted from required setup hints", errOut.String()) + } if strings.Contains(out.String()+errOut.String(), "private-key-value") { t.Fatalf("command output leaked secret: stdout=%q stderr=%q", out.String(), errOut.String()) } @@ -3820,6 +3823,36 @@ func TestWriteInitCredentialPlanHintsUsesMissingRequiredKeysOnly(t *testing.T) { } } +func TestWriteInitCredentialPlanHintsForDeferredGitHubAppUsesRequiredKeysOnly(t *testing.T) { + var stderr bytes.Buffer + entry := initCredentialPlanEntry{ + Ref: config.CredentialRef{ + Purpose: "reviewer_credentials", + Ref: "codereview/rianjs-bot", + Mode: string(config.GitAuthModeGitHubApp), + }, + KeySpecs: []credentials.KeySpec{ + {Key: credentials.GitHubAppIDKey, Required: true}, + {Key: credentials.GitHubAppPrivateKeyKey, Required: true}, + {Key: credentials.GitHubAppInstallationIDKey, Required: false}, + }, + State: initCredentialPlanStateDefer, + } + + if err := writeInitCredentialPlanHints(&stderr, "", entry); err != nil { + t.Fatalf("writeInitCredentialPlanHints: %v", err) + } + got := stderr.String() + for _, key := range []string{credentials.GitHubAppIDKey, credentials.GitHubAppPrivateKeyKey} { + if !strings.Contains(got, "cr set-credential --ref codereview/rianjs-bot --key "+key+" --stdin") { + t.Fatalf("stderr = %q, want required setup hint for %s", got, key) + } + } + if strings.Contains(got, "--key "+credentials.GitHubAppInstallationIDKey+" --stdin") { + t.Fatalf("stderr = %q, want optional installation id omitted from required setup hints", got) + } +} + func TestHuhInitPrompterAccessiblePrefillsExistingProfile(t *testing.T) { t.Setenv("TERM", "dumb") existing := apiKeyProfile("work", config.LLMProviderOpenAI) @@ -6257,6 +6290,56 @@ func TestHuhInitReviewerEntityPrompterDefaultUsesLinearReviewerFlow(t *testing.T } } +func TestHuhInitReviewerEntityPrompterGitHubAppLinearFlowShowsCredentialBundleCopy(t *testing.T) { + existing := basicProfile("work") + var stderr bytes.Buffer + prompter := huhInitReviewerEntityPrompter{ + stderr: &stderr, + editorRunner: func(editor initLinearEditor, _ io.Reader, out io.Writer) (initLinearEditorModel, error) { + model := newInitLinearEditorModel(editor, 160, 60) + model = selectInitLinearFieldValue(t, model, initReviewerEntityFieldSelection, string(initReviewerEntityKindGitHubApp)) + model = focusInitLinearField(t, model, initReviewerEntityFieldAction) + model = selectInitLinearFieldValue(t, model, initReviewerEntityFieldAction, initDetailActionEdit) + _, _ = io.WriteString(out, model.View()) + updated, _ := model.Update(tea.KeyMsg{Type: tea.KeyEnter}) + next, ok := updated.(initLinearEditorModel) + if !ok { + t.Fatalf("Update returned %T, want initLinearEditorModel", updated) + } + return next, nil + }, + } + + draft, err := prompter.EditReviewerEntity(initReviewerEntityPrompt{Context: initPromptContext{ + RequestedProfileName: "work", + ExistingProfileName: "work", + ExistingProfile: &existing, + DefaultProfileName: "work", + ExistingConfig: config.File{Profiles: map[string]config.Profile{"work": existing}}, + }}) + if err != nil { + t.Fatalf("EditReviewerEntity: %v", err) + } + if !draft.ReviewerEnabled || draft.ReviewerAuth != string(config.GitAuthModeGitHubApp) { + t.Fatalf("draft = %#v, want GitHub App reviewer", draft) + } + out := stderr.String() + for _, want := range []string{ + "GitHub App reviewer. Required credential keys", + "Reviewer secret location", + "non-secret", + "credential-store ref", + credentials.GitHubAppIDKey, + credentials.GitHubAppPrivateKeyKey, + "Optional credential key: " + credentials.GitHubAppInstallationIDKey, + credentials.GitHubAppInstallationIDKey + " is optional", + } { + if !strings.Contains(out, want) { + t.Fatalf("stderr missing %q:\n%s", want, out) + } + } +} + func TestHuhInitReviewerEntityDetailsBackDoesNotMutateDraft(t *testing.T) { t.Setenv("TERM", "dumb") draft := seedInteractiveInitDraft("work", "work", "work", nil) @@ -6282,6 +6365,42 @@ func TestHuhInitReviewerEntityDetailsBackDoesNotMutateDraft(t *testing.T) { } } +func TestHuhInitReviewerEntityDetailsGitHubAppShowsCredentialBundleCopy(t *testing.T) { + t.Setenv("TERM", "dumb") + draft := seedInteractiveInitDraft("work", "work", "work", nil) + var stderr bytes.Buffer + prompter := huhInitReviewerEntityPrompter{ + stderr: &stderr, + editorRunner: stageReviewerEntityEditorRunner(t, nil, ""), + } + + nextDraft, back, err := prompter.editNewReviewerEntity(initReviewerEntityKindGitHubApp, draft) + if err != nil { + t.Fatalf("editNewReviewerEntity: %v", err) + } + if back { + t.Fatal("back = true, want staged GitHub App reviewer details") + } + if !nextDraft.ReviewerEnabled || nextDraft.ReviewerAuth != string(config.GitAuthModeGitHubApp) { + t.Fatalf("draft = %#v, want GitHub App reviewer", nextDraft) + } + out := stderr.String() + for _, want := range []string{ + "GitHub App reviewer. Required credential keys", + "Reviewer secret location", + "non-secret", + "credential-store ref", + credentials.GitHubAppIDKey, + credentials.GitHubAppPrivateKeyKey, + "Optional credential key: " + credentials.GitHubAppInstallationIDKey, + credentials.GitHubAppInstallationIDKey + " is optional", + } { + if !strings.Contains(out, want) { + t.Fatalf("stderr missing %q:\n%s", want, out) + } + } +} + func TestHuhInitReviewerEntityDetailsAccessibleHidesSecretLocationForGitIdentity(t *testing.T) { t.Setenv("TERM", "dumb") draft := seedInteractiveInitDraft("work", "work", "work", nil) @@ -6741,6 +6860,82 @@ func TestInitCredentialReadinessNoteNamesSelectedSecretsProfile(t *testing.T) { if !strings.Contains(note, "Git via Team Vault deferred") { t.Fatalf("note = %q, want named selected secrets-management profile", note) } + if strings.Contains(note, "required:") { + t.Fatalf("note = %q, want single-key deferred credentials to keep concise readiness copy", note) + } +} + +func TestInitCredentialReadinessNoteLabelsGitHubAppRequiredAndOptionalKeys(t *testing.T) { + note := initCredentialReadinessNote(initCredentialPlanEntry{ + Ref: config.CredentialRef{ + Purpose: "reviewer_credentials", + Ref: "codereview/rianjs-bot", + Mode: string(config.GitAuthModeGitHubApp), + }, + KeySpecs: []credentials.KeySpec{ + {Key: credentials.GitHubAppIDKey, Required: true}, + {Key: credentials.GitHubAppPrivateKeyKey, Required: true}, + {Key: credentials.GitHubAppInstallationIDKey, Required: false}, + }, + State: initCredentialPlanStateDefer, + }) + for _, want := range []string{ + "reviewer deferred", + "required: " + credentials.GitHubAppIDKey + ", " + credentials.GitHubAppPrivateKeyKey, + "optional: " + credentials.GitHubAppInstallationIDKey, + } { + if !strings.Contains(note, want) { + t.Fatalf("note = %q, want %q", note, want) + } + } + if strings.Contains(note, "missing "+credentials.GitHubAppInstallationIDKey) || strings.Contains(note, "required: "+credentials.GitHubAppInstallationIDKey) { + t.Fatalf("note = %q, want installation id labeled optional only", note) + } +} + +func TestInitCredentialReadinessNoteLabelsPartialGitHubAppOptionalKey(t *testing.T) { + note := initCredentialReadinessNote(initCredentialPlanEntry{ + Ref: config.CredentialRef{ + Purpose: "reviewer_credentials", + Ref: "codereview/rianjs-bot", + Mode: string(config.GitAuthModeGitHubApp), + }, + KeySpecs: []credentials.KeySpec{ + {Key: credentials.GitHubAppIDKey, Required: true}, + {Key: credentials.GitHubAppPrivateKeyKey, Required: true}, + {Key: credentials.GitHubAppInstallationIDKey, Required: false}, + }, + MissingRequiredKeys: []string{credentials.GitHubAppPrivateKeyKey}, + State: initCredentialPlanStateMissingRequired, + }) + for _, want := range []string{ + "reviewer missing required " + credentials.GitHubAppPrivateKeyKey, + "optional: " + credentials.GitHubAppInstallationIDKey, + } { + if !strings.Contains(note, want) { + t.Fatalf("note = %q, want %q", note, want) + } + } + if strings.Contains(note, "missing "+credentials.GitHubAppInstallationIDKey) || strings.Contains(note, "required "+credentials.GitHubAppInstallationIDKey) { + t.Fatalf("note = %q, want optional installation id not treated as required", note) + } +} + +func TestInitProfileReadinessLineIncludesNotes(t *testing.T) { + line := initProfileReadinessLine(initProfileReadiness{ + ProfileName: "work", + Ready: false, + Notes: []string{"reviewer deferred (required: github_app_id, github_app_private_key; optional: github_app_installation_id)"}, + }) + for _, want := range []string{ + "- work: needs follow-up", + "required: github_app_id, github_app_private_key", + "optional: github_app_installation_id", + } { + if !strings.Contains(line, want) { + t.Fatalf("line = %q, want %q", line, want) + } + } } func TestBuildInteractiveInitWorkspaceRepairsBrokenSecretsProfileSelection(t *testing.T) { @@ -13563,6 +13758,114 @@ func TestInitInteractiveMenuFocusedReviewerEntityRebuildsSecretPlanning(t *testi } } +func TestInitInteractiveMenuFocusedGitHubAppReviewerDeferEmitsReadinessAndHints(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.yml") + cfg := config.File{ + DefaultProfile: "work", + Profiles: map[string]config.Profile{ + "work": { + Git: config.GitConfig{ + Host: "github.com", + AuthMode: config.GitAuthModePAT, + CredentialRef: "codereview/work", + }, + LLM: config.LLMConfig{ + Provider: config.LLMProviderAnthropic, + Auth: config.LLMAuthSubscription, + Adapter: config.LLMAdapterClaudeCLI, + }, + }, + }, + } + saveCredentialTestConfig(t, path, cfg) + var stdout bytes.Buffer + var stderr bytes.Buffer + opts := &root.Options{ + Stdin: strings.NewReader(""), + Stdout: &stdout, + Stderr: &stderr, + ConfigPath: path, + } + reviewerPrompterCalls := 0 + deps := initDeps{ + menuPrompter: &fakeInitMenuPrompter{ + actions: []initMenuAction{ + initMenuActionReviewerEntities, + initMenuActionSave, + }, + }, + finalizePrompter: initFinalizePrompterFunc(func(initFinalizePrompt) (initFinalizeAction, error) { + return initFinalizeActionSave, nil + }), + reviewerPrompter: initReviewerEntityPrompterFunc(func(prompt initReviewerEntityPrompt) (initDraft, error) { + reviewerPrompterCalls++ + if reviewerPrompterCalls > 1 { + return initDraft{}, errInitNavigateBack + } + draft := seedInteractiveInitDraft(prompt.Context.RequestedProfileName, prompt.Context.ExistingProfileName, prompt.Context.DefaultProfileName, prompt.Context.ExistingProfile) + draft.ReviewerEnabled = true + draft.ReviewerAuth = string(config.GitAuthModeGitHubApp) + draft.ReviewerCredentialRef = "codereview/rianjs-bot" + draft.ReviewerDisplayName = "rianjs-bot" + return draft, nil + }), + secretPrompter: &fakeInitSecretPrompter{ + actions: []initCredentialSecretAction{initCredentialSecretActionDefer}, + }, + openStore: func(string, bool, config.File) (initStore, error) { + return newFakeInitStore(map[string]map[string]string{ + "work": { + credentials.GitTokenKey: "existing-token", + }, + }), nil + }, + configPath: func(*root.Options) (string, error) { return path, nil }, + loadConfig: loadConfigForInit, + saveConfig: config.Save, + } + + if err := runInitWithDeps(&cobra.Command{}, opts, initOptions{}, deps); err != nil { + t.Fatalf("runInitWithDeps: %v", err) + } + got, err := config.Load(path) + if err != nil { + t.Fatalf("Load config: %v", err) + } + profile := got.Profiles["work"] + if profile.ReviewerCredentials == nil { + t.Fatal("reviewer credentials = nil, want deferred GitHub App reviewer saved") + } + if profile.ReviewerCredentials.AuthMode != config.GitAuthModeGitHubApp || + profile.ReviewerCredentials.CredentialRef != "codereview/rianjs-bot" || + profile.ReviewerCredentials.DisplayName != "rianjs-bot" { + t.Fatalf("reviewer credentials = %#v, want rianjs-bot GitHub App reviewer", profile.ReviewerCredentials) + } + out := stdout.String() + for _, want := range []string{ + "Saved staged init changes", + "- work: needs follow-up", + "reviewer deferred", + "required: " + credentials.GitHubAppIDKey + ", " + credentials.GitHubAppPrivateKeyKey, + "optional: " + credentials.GitHubAppInstallationIDKey, + } { + if !strings.Contains(out, want) { + t.Fatalf("stdout = %q, want %q", out, want) + } + } + errOut := stderr.String() + for _, want := range []string{ + "Next: cr set-credential --ref codereview/rianjs-bot --key " + credentials.GitHubAppIDKey + " --stdin", + "Next: cr set-credential --ref codereview/rianjs-bot --key " + credentials.GitHubAppPrivateKeyKey + " --stdin", + } { + if !strings.Contains(errOut, want) { + t.Fatalf("stderr = %q, want %q", errOut, want) + } + } + if strings.Contains(errOut, " --key "+credentials.GitHubAppInstallationIDKey+" ") { + t.Fatalf("stderr = %q, want optional installation id omitted from required follow-up commands", errOut) + } +} + func TestInitInteractiveMenuFocusedReviewerEntitySavePreservesCustomCredentialRef(t *testing.T) { path := filepath.Join(t.TempDir(), "config.yml") cfg := config.File{ diff --git a/internal/cmd/credentialcmd/init_reviewer_entity_editor.go b/internal/cmd/credentialcmd/init_reviewer_entity_editor.go index c04e152..92a28b0 100644 --- a/internal/cmd/credentialcmd/init_reviewer_entity_editor.go +++ b/internal/cmd/credentialcmd/init_reviewer_entity_editor.go @@ -8,6 +8,8 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/huh" + + "github.com/open-cli-collective/codereview-cli/internal/credentials" ) type initReviewerEntityEditorRunner func(initLinearEditor, io.Reader, io.Writer) (initLinearEditorModel, error) @@ -132,7 +134,7 @@ func newReviewerEntityEditorState(entity initReviewerEntityDraft, seed initDraft func (s reviewerEntityEditorState) editor() initLinearEditor { var document initLinearDocument document.addSection("Reviewer entity", reviewerEntitySelectionDescription()) - document.addSection("Reviewer entity type", reviewerEntityKindDetailLabel(s.kind)) + document.addSection("Reviewer entity type", reviewerEntityKindDetailDescription(s.kind)) if s.kind != initReviewerEntityKindUseGitIdentity { labelInput, _, _ := reviewerEntityEditorLabelSeed(initReviewerEntityDraft{ Kind: s.kind, @@ -163,7 +165,7 @@ func (s reviewerEntityEditorState) editor() initLinearEditor { document.addEditableInput( initReviewerEntityFieldSecretLocation, "Reviewer secret location", - "Leave blank to use the standard reviewer secret location for this profile. Replace the value only if you need a custom location.", + reviewerEntitySecretLocationDescription(s.kind), reviewerSecretLocation, validateOptionalCredentialRef, ) @@ -251,7 +253,7 @@ func initReviewerEntityLinearEditor(ctx initPromptContext, seed initDraft) initL document.addEditableInput( initReviewerEntityFieldSecretLocation, "Reviewer secret location", - "Leave blank to use the standard reviewer secret location for this profile. Replace the value only if you need a custom location.", + reviewerEntitySecretLocationDescription(state.kind), "", validateOptionalCredentialRef, ) @@ -373,7 +375,7 @@ func initReviewerEntitySyncLinearFields(model *initLinearEditorModel, ctx initPr } detailsIndex := model.document.fieldIndexByTitle("Reviewer details") if detailsIndex >= 0 { - model.document[detailsIndex].Description = reviewerEntityKindDetailLabel(state.kind) + model.document[detailsIndex].Description = reviewerEntityKindDetailDescription(state.kind) } hideDetails := state.kind == initReviewerEntityKindUseGitIdentity if _, restore := initReviewerEntityRestoreSelectionName(selection); restore { @@ -384,6 +386,9 @@ func initReviewerEntitySyncLinearFields(model *initLinearEditorModel, ctx initPr } model.setFieldHidden(initReviewerEntityFieldLabel, hideDetails) model.setFieldHidden(initReviewerEntityFieldSecretLocation, hideDetails) + if !hideDetails { + model.setFieldDescription(initReviewerEntityFieldSecretLocation, reviewerEntitySecretLocationDescription(state.kind)) + } if resetDetails && !hideDetails { labelInput, _, _ := reviewerEntityEditorLabelSeed(initReviewerEntityDraft{ Kind: state.kind, @@ -441,6 +446,22 @@ func reviewerEntityEditorStateForSelection(ctx initPromptContext, seed initDraft return newReviewerEntityEditorState(initReviewerEntityDraft{Kind: initReviewerEntityKind(selection)}, candidate, false) } +func reviewerEntityKindDetailDescription(kind initReviewerEntityKind) string { + label := reviewerEntityKindDetailLabel(kind) + if kind != initReviewerEntityKindGitHubApp { + return label + } + return label + ". Required credential keys: " + credentials.GitHubAppIDKey + ", " + credentials.GitHubAppPrivateKeyKey + ". Optional credential key: " + credentials.GitHubAppInstallationIDKey + "." +} + +func reviewerEntitySecretLocationDescription(kind initReviewerEntityKind) string { + base := "Leave blank to use the standard reviewer secret location for this profile. Replace the value only if you need a custom location." + if kind != initReviewerEntityKindGitHubApp { + return base + } + return base + " This is a non-secret credential-store ref such as codereview/work-reviewer, not the GitHub App ID or private key. Store required secrets " + credentials.GitHubAppIDKey + " and " + credentials.GitHubAppPrivateKeyKey + " at this ref; " + credentials.GitHubAppInstallationIDKey + " is optional." +} + func initReviewerEntityDraftFromDocument(ctx initPromptContext, seed initDraft, document initLinearDocument) (initDraft, error) { selection := document.selectedValue(initReviewerEntityFieldSelection) state, err := reviewerEntityEditorStateForSelection(ctx, seed, selection)