Skip to content

Commit b2cf7f6

Browse files
authored
Merge pull request #1818 from entireio/feat/bootstrap-one-question
feat(enable): collapse the non-git bootstrap into one setup question
2 parents df97a55 + f18a104 commit b2cf7f6

2 files changed

Lines changed: 275 additions & 33 deletions

File tree

cmd/entire/cli/setup_github.go

Lines changed: 121 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,19 @@ var ghRepoNameRe = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)
104104

105105
// allowed visibility values.
106106
const (
107-
visibilityPublic = "public"
108-
visibilityPrivate = "private"
109-
visibilityInternal = "internal"
107+
visibilityPublic = "public"
108+
visibilityPrivate = "private"
109+
visibilityInternal = "internal"
110+
defaultInitialCommitMessage = "Initial commit"
111+
)
112+
113+
type bootstrapSetupChoice string
114+
115+
const (
116+
bootstrapSetupLocal bootstrapSetupChoice = "local"
117+
bootstrapSetupGitHub bootstrapSetupChoice = "github"
118+
bootstrapSetupCustom bootstrapSetupChoice = "custom"
119+
bootstrapSetupDecline bootstrapSetupChoice = "decline"
110120
)
111121

112122
// bootstrapState carries pre-setup decisions into the post-setup finalize
@@ -146,13 +156,29 @@ func runGitHubBootstrapInitWith(ctx context.Context, w, errW io.Writer, opts Git
146156
return nil, fmt.Errorf("get working directory: %w", err)
147157
}
148158

149-
// Step 1: confirm we should git init here.
150-
proceed, err := confirmInitRepo(w, cwd, opts)
151-
if err != nil {
152-
return nil, err
153-
}
154-
if !proceed {
155-
return nil, errBootstrapDeclined
159+
// Step 1: decide whether to init here — and, on the bare interactive
160+
// path, how: one select carries both the init consent and the setup
161+
// preset, so the common flow costs a single answer. Explicit flags,
162+
// --yes, and non-interactive runs keep the granular confirm + resolver
163+
// contracts unchanged.
164+
setupChoice := bootstrapSetupCustom
165+
if shouldPromptBootstrapSetupChoice(opts) {
166+
githubReady := ghAvailable(ctx, runner) && ghAuthenticated(ctx, runner)
167+
setupChoice, err = promptBootstrapSetupChoice(w, cwd, githubReady)
168+
if err != nil {
169+
return nil, err
170+
}
171+
if setupChoice == bootstrapSetupDecline {
172+
return nil, errBootstrapDeclined
173+
}
174+
} else {
175+
proceed, confirmErr := confirmInitRepo(w, cwd, opts)
176+
if confirmErr != nil {
177+
return nil, confirmErr
178+
}
179+
if !proceed {
180+
return nil, errBootstrapDeclined
181+
}
156182
}
157183

158184
// Step 2: git init.
@@ -165,13 +191,12 @@ func runGitHubBootstrapInitWith(ctx context.Context, w, errW io.Writer, opts Git
165191
paths.ClearWorktreeRootCache()
166192
fmt.Fprintln(w, " ✓ Initialized empty git repository")
167193

168-
// Step 3: decide whether to create a GitHub repo. Creating a remote is an
169-
// explicit opt-in: it happens only on an explicit signal (repo flags,
170-
// --push, or --yes) or an interactive "yes". A non-interactive run with no
171-
// such signal stays local-only — we never create a repo on the user's
172-
// behalf. --no-github always wins.
173-
useGitHub := false
174-
if !opts.NoGitHub {
194+
// Creating a remote remains an explicit opt-in. Choosing the GitHub preset
195+
// is that consent: its label states that the private repository will be
196+
// created and the initial commit pushed. The local preset never creates or
197+
// pushes a remote.
198+
useGitHub := setupChoice == bootstrapSetupGitHub
199+
if setupChoice == bootstrapSetupCustom && !opts.NoGitHub {
175200
explicit := ghCreateRequested(opts)
176201
// Only probe gh (and warn about a missing/unauthenticated CLI) when the
177202
// user actually wants a GitHub repo — explicitly, or via the confirm
@@ -187,7 +212,6 @@ func runGitHubBootstrapInitWith(ctx context.Context, w, errW io.Writer, opts Git
187212
case explicit:
188213
useGitHub = true
189214
default:
190-
// Interactive with no explicit signal: prompt, defaulting to No.
191215
confirmed, err := confirmCreateGitHubRepo(cwd)
192216
if err != nil {
193217
return nil, err
@@ -201,7 +225,13 @@ func runGitHubBootstrapInitWith(ctx context.Context, w, errW io.Writer, opts Git
201225
// contiguous.
202226
var fullName, visibility string
203227
if useGitHub {
204-
owner, name, vis, err := selectGitHubRepo(ctx, w, errW, runner, cwd, opts)
228+
repoOpts := opts
229+
if setupChoice == bootstrapSetupGitHub {
230+
// The GitHub preset resolves the current user, folder-derived name,
231+
// and private visibility without reopening the granular prompts.
232+
repoOpts.Yes = true
233+
}
234+
owner, name, vis, err := selectGitHubRepo(ctx, w, errW, runner, cwd, repoOpts)
205235
if err != nil {
206236
return nil, err
207237
}
@@ -216,9 +246,12 @@ func runGitHubBootstrapInitWith(ctx context.Context, w, errW io.Writer, opts Git
216246
// because gh may read local config; but we can skip the identity
217247
// check when the user is fully opting out of both commit and
218248
// remote to keep the flow minimal.
219-
message, commit, err := resolveCommitMessage(opts)
220-
if err != nil {
221-
return nil, err
249+
message, commit := defaultInitialCommitMessage, true
250+
if setupChoice == bootstrapSetupCustom {
251+
message, commit, err = resolveCommitMessage(opts)
252+
if err != nil {
253+
return nil, err
254+
}
222255
}
223256
if commit {
224257
if err := ensureGitIdentity(ctx, w, errW, runner, cwd); err != nil {
@@ -234,6 +267,9 @@ func runGitHubBootstrapInitWith(ctx context.Context, w, errW io.Writer, opts Git
234267
push := false
235268
if useGitHub && commit {
236269
switch {
270+
case setupChoice == bootstrapSetupGitHub:
271+
// The preset label explicitly includes pushing the initial commit.
272+
push = true
237273
case opts.Yes || opts.Push:
238274
push = true
239275
case interactive.CanPromptInteractively():
@@ -429,6 +465,65 @@ func confirmInitRepo(_ io.Writer, cwd string, opts GitHubBootstrapOptions) (bool
429465
return confirmed, nil
430466
}
431467

468+
// shouldPromptBootstrapSetupChoice reports whether this is the bare
469+
// interactive bootstrap path. Any option that expresses a granular choice —
470+
// including --init-repo / --no-init-repo, which answer the init consent the
471+
// merged select carries — keeps the established flag behavior instead of
472+
// being overwritten by a preset.
473+
func shouldPromptBootstrapSetupChoice(opts GitHubBootstrapOptions) bool {
474+
return interactive.CanPromptInteractively() &&
475+
!opts.Yes &&
476+
!opts.InitRepo &&
477+
!opts.NoInitRepo &&
478+
!opts.NoGitHub &&
479+
!ghFlagsProvided(opts) &&
480+
!opts.Push &&
481+
opts.InitialCommitMessage == "" &&
482+
!opts.SkipInitialCommit
483+
}
484+
485+
// promptBootstrapSetupChoice merges the init consent and the common
486+
// bootstrap decisions into one select, so the bare interactive path costs a
487+
// single answer. It runs _before_ `git init`: declining — including Ctrl-C —
488+
// leaves the folder untouched. The selected commit is still deferred until
489+
// Entire has written its settings and agent configuration.
490+
//
491+
// The wrong-directory guard from the granular confirm (issue #1717) carries
492+
// over: the absolute path stays in the title (the accessible renderer drops
493+
// descriptions), and the menu makes the choice visible before Enter lands on
494+
// the recommended preset.
495+
func promptBootstrapSetupChoice(w io.Writer, cwd string, githubReady bool) (bootstrapSetupChoice, error) {
496+
options := []huh.Option[bootstrapSetupChoice]{
497+
huh.NewOption("Yes, with one initial commit (recommended)", bootstrapSetupLocal),
498+
}
499+
if githubReady {
500+
options = append(options,
501+
huh.NewOption("Yes, plus a private GitHub repository (pushed)", bootstrapSetupGitHub),
502+
)
503+
}
504+
options = append(options,
505+
huh.NewOption("Yes, customize...", bootstrapSetupCustom),
506+
huh.NewOption("No", bootstrapSetupDecline),
507+
)
508+
509+
choice := bootstrapSetupLocal
510+
form := NewAccessibleForm(
511+
huh.NewGroup(
512+
huh.NewSelect[bootstrapSetupChoice]().
513+
Title(fmt.Sprintf("No git repository in %q. Set one up?", cwd)).
514+
Options(options...).
515+
Value(&choice),
516+
),
517+
).WithOutput(w)
518+
if err := form.Run(); err != nil {
519+
if errors.Is(err, huh.ErrUserAborted) {
520+
return bootstrapSetupDecline, nil
521+
}
522+
return "", fmt.Errorf("git setup prompt: %w", err)
523+
}
524+
return choice, nil
525+
}
526+
432527
// selectGitHubRepo gathers owner, repo name, and visibility, respecting
433528
// supplied flags and falling back to interactive prompts.
434529
func selectGitHubRepo(ctx context.Context, w, errW io.Writer, runner bootstrapRunner, cwd string, opts GitHubBootstrapOptions) (owner, name, visibility string, err error) {
@@ -633,16 +728,14 @@ func resolveVisibility(owner, currentUser string, opts GitHubBootstrapOptions) (
633728
// the initial commit entirely; callers must skip `doInitialCommit` and
634729
// any subsequent push.
635730
func resolveCommitMessage(opts GitHubBootstrapOptions) (string, bool, error) {
636-
const defaultMsg = "Initial commit"
637-
638731
if opts.SkipInitialCommit {
639732
return "", false, nil
640733
}
641734
if opts.InitialCommitMessage != "" {
642735
return opts.InitialCommitMessage, true, nil
643736
}
644737
if opts.Yes || !interactive.CanPromptInteractively() {
645-
return defaultMsg, true, nil
738+
return defaultInitialCommitMessage, true, nil
646739
}
647740

648741
const (
@@ -674,7 +767,7 @@ func resolveCommitMessage(opts GitHubBootstrapOptions) (string, bool, error) {
674767
case choiceSkip:
675768
return "", false, nil
676769
case choiceCustomize:
677-
input := defaultMsg
770+
input := defaultInitialCommitMessage
678771
custom := NewAccessibleForm(
679772
huh.NewGroup(
680773
huh.NewInput().
@@ -689,11 +782,11 @@ func resolveCommitMessage(opts GitHubBootstrapOptions) (string, bool, error) {
689782
return "", false, fmt.Errorf("commit message prompt: %w", err)
690783
}
691784
if strings.TrimSpace(input) == "" {
692-
return defaultMsg, true, nil
785+
return defaultInitialCommitMessage, true, nil
693786
}
694787
return input, true, nil
695788
default:
696-
return defaultMsg, true, nil
789+
return defaultInitialCommitMessage, true, nil
697790
}
698791
}
699792

0 commit comments

Comments
 (0)