Skip to content

Move CLI bootstrap into internal package#306

Merged
kindermax merged 1 commit intomasterfrom
codex/move-lets-entry-to-internal
Mar 19, 2026
Merged

Move CLI bootstrap into internal package#306
kindermax merged 1 commit intomasterfrom
codex/move-lets-entry-to-internal

Conversation

@kindermax
Copy link
Collaborator

@kindermax kindermax commented Mar 19, 2026

Summary

  • relocate the lets startup logic into internal/cli/cli.go and expose a Main helper that mirrors the gh CLI entry pattern
  • keep cmd/lets/main.go as a thin launcher that simply forwards Version/BuildDate to the new helper and exits with its code
  • update the changelog to note the refactoring

Testing

  • Not run (not requested)

Summary by Sourcery

Move the CLI startup flow into an internal cli package and keep the binary entrypoint as a thin launcher delegating to it.

Enhancements:

  • Introduce an internal cli.Main helper that encapsulates the full CLI bootstrap and execution flow, returning an exit code.
  • Simplify cmd/lets/main.go to just pass version metadata into the internal CLI helper and exit with its code.

Documentation:

  • Update the changelog to document the refactoring of CLI startup into the internal cli package.

@sourcery-ai
Copy link

sourcery-ai bot commented Mar 19, 2026

Reviewer's Guide

Refactors the CLI startup flow by moving all bootstrapping and flag-handling logic from the public cmd/lets entrypoint into an internal cli package with a reusable Main(version, buildDate) function, leaving main.go as a thin shim, and documents the change in the changelog.

Class diagram for internal cli package structure

classDiagram
    class cli {
        +int Main(version string, buildDate string)
        -context.Context getContext()
        -int getExitCode(err error, defaultCode int)
        -bool failOnConfigError(root *cobra.Command, current *cobra.Command, rootFlags *flags)
        -*flags parseRootFlags(args []string) error
    }

    class flags {
        +string config
        +int debug
        +bool help
        +bool version
        +bool all
        +bool init
        +bool upgrade
    }

    cli --> flags : uses
    cli ..> cobra_Command : uses
    cli ..> executor_DependencyError : inspects
    cli ..> cmd : uses
    cli ..> config : uses
    cli ..> env : uses
    cli ..> logging : uses
    cli ..> set : uses
    cli ..> upgrade : uses
    cli ..> registry : uses
    cli ..> workdir : uses

    class cobra_Command {
        +InitDefaultHelpFlag()
        +InitDefaultVersionFlag()
        +InitDefaultHelpCmd()
        +Traverse(args []string)
        +ExecuteContext(ctx context.Context) error
        +Flags() *pflagFlagSet
        +Name() string
    }

    class executor_DependencyError {
        +error Error
        +ExitCode() int
    }

    class pflagFlagSet {
        +int NFlag()
    }
Loading

File-Level Changes

Change Details Files
Introduce internal cli.Main helper that encapsulates all CLI startup, flag parsing, and command execution logic and returns an exit code instead of calling os.Exit directly.
  • Create a new internal package that contains the previous main.go logic, parameterized by version and build date.
  • Preserve existing behavior for context setup, logging, root command construction, completion/self/init/upgrade handling, and help/version flows.
  • Return integer exit codes from Main via existing getExitCode helper instead of exiting the process directly in most code paths.
internal/cli/cli.go
Convert the public binary entrypoint into a thin launcher that delegates to the internal CLI helper.
  • Remove all CLI bootstrapping code from the entrypoint file, keeping only Version and BuildDate variables.
  • Call cli.Main(Version, BuildDate) and pass its return value to os.Exit to terminate the process.
cmd/lets/main.go
Document the refactoring in the project changelog.
  • Add a changelog entry under the Unreleased section describing the move of CLI startup flow into internal/cli/cli.go and the simplification of main.go.
docs/docs/changelog.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • Now that the startup logic lives in internal/cli, consider decoupling it from process globals (os.Args, os.Stdout/Stderr, environment) by passing these in as parameters to Main (or a lower-level helper) to make the CLI flow easier to test and reuse.
  • Main currently hardcodes reading from os.Args again; since you already have the traversed args from rootCmd.Traverse, it may be cleaner to pass those into any helpers that need them rather than reaching back to os.Args for checks like len(os.Args) == 1.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Now that the startup logic lives in internal/cli, consider decoupling it from process globals (os.Args, os.Stdout/Stderr, environment) by passing these in as parameters to Main (or a lower-level helper) to make the CLI flow easier to test and reuse.
- Main currently hardcodes reading from os.Args again; since you already have the traversed args from rootCmd.Traverse, it may be cleaner to pass those into any helpers that need them rather than reaching back to os.Args for checks like `len(os.Args) == 1`.

## Individual Comments

### Comment 1
<location path="internal/cli/cli.go" line_range="190-199" />
<code_context>
-//	cobra will parse all --config flags, but take only latest
-//
-// --config=myconfig, and this is wrong.
-func parseRootFlags(args []string) (*flags, error) {
-	f := &flags{}
-	// if first arg is not a flag, then it is subcommand
-	if len(args) > 0 && !strings.HasPrefix(args[0], "-") {
-		return f, nil
-	}
-
-	visited := set.NewSet[string]()
-
-	isFlagVisited := func(name string) bool {
-		if visited.Contains(name) {
-			return true
-		}
-
-		visited.Add(name)
-
-		return false
-	}
-
-	idx := 0
-	for idx < len(args) {
-		arg := args[idx]
-		if !strings.HasPrefix(arg, "-") {
-			// stop if arg is not a flag, it is probably a subcommand
-			break
-		}
-
-		name, value, found := strings.Cut(arg, "=")
-		switch name {
-		case "--config", "-c":
-			if !isFlagVisited("config") {
-				if found {
</code_context>
<issue_to_address>
**issue (bug_risk):** Guard against out-of-bounds access when parsing --config without an inline value.

When `--config` is the last argument (e.g. `lets --config`), `len(args[idx:]) > 0` can be true while `args[idx+1]` is out of range, causing a panic. Tighten the check to ensure there is a next argument, e.g. `len(args) > idx+1` or `len(args[idx:]) > 1`, so you can return a proper error instead of panicking.
</issue_to_address>

### Comment 2
<location path="internal/cli/cli.go" line_range="134" />
<code_context>
-	}
-}
-
-// getContext returns context and kicks of a goroutine
-// which waits for SIGINT, SIGTERM and cancels global context.
-//
</code_context>
<issue_to_address>
**nitpick (typo):** Minor typo in comment: "kicks of" should be "kicks off".

In the comment above `getContext`, please change "kicks of a goroutine" to "kicks off a goroutine".

```suggestion
// getContext returns context and kicks off a goroutine
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +190 to +199
func parseRootFlags(args []string) (*flags, error) {
f := &flags{}
// if first arg is not a flag, then it is subcommand
if len(args) > 0 && !strings.HasPrefix(args[0], "-") {
return f, nil
}

visited := set.NewSet[string]()

isFlagVisited := func(name string) bool {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Guard against out-of-bounds access when parsing --config without an inline value.

When --config is the last argument (e.g. lets --config), len(args[idx:]) > 0 can be true while args[idx+1] is out of range, causing a panic. Tighten the check to ensure there is a next argument, e.g. len(args) > idx+1 or len(args[idx:]) > 1, so you can return a proper error instead of panicking.

return 0
}

// getContext returns context and kicks of a goroutine
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (typo): Minor typo in comment: "kicks of" should be "kicks off".

In the comment above getContext, please change "kicks of a goroutine" to "kicks off a goroutine".

Suggested change
// getContext returns context and kicks of a goroutine
// getContext returns context and kicks off a goroutine

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 19, 2026

Greptile Summary

This PR is a mechanical refactoring that moves the lets CLI bootstrap logic from cmd/lets/main.go into a new internal/cli/cli.go package, exposing a Main(version, buildDate string) int helper that returns an exit code. cmd/lets/main.go is reduced to a thin four-line launcher that forwards the injected build-time variables and calls os.Exit with the returned code. No functional behaviour is changed.

Key changes:

  • New internal/cli/cli.go package containing the full CLI startup flow (signal handling, config loading, flag parsing, command dispatch).
  • cmd/lets/main.go trimmed to a minimal launcher — Version/BuildDate ldflags injection targets are preserved in the correct package.
  • docs/docs/changelog.md updated to record the refactoring.

Review notes:

  • The code move is 1-to-1; all logic is identical to the deleted code. No new bugs are introduced.
  • Version/BuildDate remain as package-level vars in main so existing -ldflags="-X main.Version=..." build scripts continue to work without any changes.
  • The pattern matches the gh CLI idiom referenced in the PR description.

Confidence Score: 5/5

  • This PR is safe to merge — it is a purely mechanical code move with no functional changes.
  • The entire change is a 1-to-1 relocation of existing, tested logic from main.go into internal/cli/cli.go. The thin launcher pattern is idiomatic in Go CLI projects (mirrors gh). No new logic is introduced, no interfaces are changed, and ldflags injection targets are preserved correctly in the main package.
  • No files require special attention.

Important Files Changed

Filename Overview
internal/cli/cli.go New file containing the full CLI bootstrap, moved verbatim from main.go. Logic is correct; minor pre-existing nit: the signal-handler goroutine in getContext() is never stopped via signal.Stop, which would leak in a test context if Main() were ever called more than once.
cmd/lets/main.go Reduced to a minimal 4-line launcher. Version/BuildDate ldflags targets are correctly preserved in the main package.
docs/docs/changelog.md Changelog entry added to record the refactoring; no issues.

Sequence Diagram

sequenceDiagram
    participant M as cmd/lets/main.go
    participant C as internal/cli/cli.go
    participant R as internal/cmd (rootCmd)
    participant CF as internal/config
    participant E as internal/executor

    M->>C: cli.Main(Version, BuildDate)
    C->>C: getContext() — starts signal goroutine
    C->>R: CreateRootCommand(version, buildDate)
    C->>R: Traverse(os.Args[1:])
    C->>C: parseRootFlags(args)
    alt --version flag
        C->>R: PrintVersionMessage()
        C-->>M: return 0
    else --init flag
        C->>CF: workdir.InitLetsFile()
        C-->>M: return 0
    else --upgrade flag
        C->>CF: upgrade.NewBinaryUpgrader().Upgrade()
        C-->>M: return 0
    else show usage
        C->>R: PrintRootHelpMessage()
        C-->>M: return 0
    else normal execution
        C->>CF: config.Load()
        C->>R: InitSubCommands()
        C->>R: ExecuteContext(ctx)
        R->>E: run command
        E-->>C: result/error
        C-->>M: return exitCode
    end
    M->>M: os.Exit(exitCode)
Loading

Last reviewed commit: "Move main logic into..."

@kindermax kindermax merged commit 6086d46 into master Mar 19, 2026
6 checks passed
@kindermax kindermax deleted the codex/move-lets-entry-to-internal branch March 19, 2026 08:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant