Skip to content

Commit 6086d46

Browse files
authored
Merge pull request #306 from lets-cli/codex/move-lets-entry-to-internal
Move CLI bootstrap into internal package
2 parents e24933c + abe53a0 commit 6086d46

3 files changed

Lines changed: 270 additions & 256 deletions

File tree

cmd/lets/main.go

Lines changed: 2 additions & 256 deletions
Original file line numberDiff line numberDiff line change
@@ -1,268 +1,14 @@
11
package main
22

33
import (
4-
"context"
5-
"errors"
64
"os"
7-
"os/signal"
8-
"strings"
9-
"syscall"
105

11-
"github.com/lets-cli/lets/internal/cmd"
12-
"github.com/lets-cli/lets/internal/config"
13-
"github.com/lets-cli/lets/internal/env"
14-
"github.com/lets-cli/lets/internal/executor"
15-
"github.com/lets-cli/lets/internal/logging"
16-
"github.com/lets-cli/lets/internal/set"
17-
"github.com/lets-cli/lets/internal/upgrade"
18-
"github.com/lets-cli/lets/internal/upgrade/registry"
19-
"github.com/lets-cli/lets/internal/workdir"
20-
log "github.com/sirupsen/logrus"
21-
"github.com/spf13/cobra"
6+
"github.com/lets-cli/lets/internal/cli"
227
)
238

249
var Version = "0.0.0-dev"
2510
var BuildDate = ""
2611

2712
func main() {
28-
ctx := getContext()
29-
30-
configDir := os.Getenv("LETS_CONFIG_DIR")
31-
32-
logging.InitLogging(os.Stdout, os.Stderr)
33-
34-
rootCmd := cmd.CreateRootCommand(Version, BuildDate)
35-
rootCmd.InitDefaultHelpFlag()
36-
rootCmd.InitDefaultVersionFlag()
37-
reinitCompletionCmd := cmd.InitCompletionCmd(rootCmd, nil)
38-
cmd.InitSelfCmd(rootCmd, Version)
39-
rootCmd.InitDefaultHelpCmd()
40-
41-
command, args, err := rootCmd.Traverse(os.Args[1:])
42-
if err != nil {
43-
log.Errorf("lets: traverse commands error: %s", err)
44-
os.Exit(getExitCode(err, 1))
45-
}
46-
47-
rootFlags, err := parseRootFlags(args)
48-
if err != nil {
49-
log.Errorf("lets: parse flags error: %s", err)
50-
os.Exit(1)
51-
}
52-
53-
if rootFlags.version {
54-
if err := cmd.PrintVersionMessage(rootCmd); err != nil {
55-
log.Errorf("lets: print version error: %s", err)
56-
os.Exit(1)
57-
}
58-
59-
os.Exit(0)
60-
}
61-
62-
debugLevel := env.SetDebugLevel(rootFlags.debug)
63-
64-
if debugLevel > 0 {
65-
log.SetLevel(log.DebugLevel)
66-
}
67-
68-
if rootFlags.config == "" {
69-
rootFlags.config = os.Getenv("LETS_CONFIG")
70-
}
71-
72-
cfg, err := config.Load(rootFlags.config, configDir, Version)
73-
if err != nil {
74-
if failOnConfigError(rootCmd, command, rootFlags) {
75-
log.Errorf("lets: config error: %s", err)
76-
os.Exit(1)
77-
}
78-
}
79-
80-
if cfg != nil {
81-
reinitCompletionCmd(cfg)
82-
cmd.InitSubCommands(rootCmd, cfg, rootFlags.all, os.Stdout)
83-
}
84-
85-
if rootFlags.init {
86-
wd, err := os.Getwd()
87-
if err == nil {
88-
err = workdir.InitLetsFile(wd, Version)
89-
}
90-
91-
if err != nil {
92-
log.Errorf("lets: can not create lets.yaml: %s", err)
93-
os.Exit(1)
94-
}
95-
96-
os.Exit(0)
97-
}
98-
99-
if rootFlags.upgrade {
100-
upgrader, err := upgrade.NewBinaryUpgrader(registry.NewGithubRegistry(ctx), Version)
101-
if err == nil {
102-
err = upgrader.Upgrade()
103-
}
104-
105-
if err != nil {
106-
log.Errorf("lets: can not self-upgrade binary: %s", err)
107-
os.Exit(1)
108-
}
109-
110-
os.Exit(0)
111-
}
112-
113-
showUsage := rootFlags.help || (command.Name() == "help" && len(args) == 0) || (len(os.Args) == 1)
114-
115-
if showUsage {
116-
if err := cmd.PrintRootHelpMessage(rootCmd); err != nil {
117-
log.Errorf("lets: print help error: %s", err)
118-
os.Exit(1)
119-
}
120-
121-
os.Exit(0)
122-
}
123-
124-
if err := rootCmd.ExecuteContext(ctx); err != nil {
125-
var depErr *executor.DependencyError
126-
if errors.As(err, &depErr) {
127-
executor.PrintDependencyTree(depErr, os.Stderr)
128-
}
129-
130-
log.Errorf("lets: %s", err.Error())
131-
os.Exit(getExitCode(err, 1))
132-
}
133-
}
134-
135-
// getContext returns context and kicks of a goroutine
136-
// which waits for SIGINT, SIGTERM and cancels global context.
137-
//
138-
// Note that since we setting stdin to command we run, that command
139-
// will receive SIGINT, SIGTERM at the same time as we here,
140-
// so command's process can begin finishing earlier than cancel will say it to.
141-
func getContext() context.Context {
142-
ch := make(chan os.Signal, 1)
143-
signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
144-
145-
ctx, cancel := context.WithCancel(context.Background())
146-
147-
go func() {
148-
sig := <-ch
149-
log.Printf("lets: signal received: %s", sig)
150-
cancel()
151-
}()
152-
153-
return ctx
154-
}
155-
156-
func getExitCode(err error, defaultCode int) int {
157-
var exitCoder interface{ ExitCode() int }
158-
if errors.As(err, &exitCoder) {
159-
return exitCoder.ExitCode()
160-
}
161-
162-
return defaultCode
163-
}
164-
165-
// do not fail on config error in it is help (-h, --help) or --init or completion command.
166-
func failOnConfigError(root *cobra.Command, current *cobra.Command, rootFlags *flags) bool {
167-
rootCommands := set.NewSet("completion", "help", "lsp")
168-
return (root.Flags().NFlag() == 0 && !rootCommands.Contains(current.Name())) && !rootFlags.help && !rootFlags.init
169-
}
170-
171-
type flags struct {
172-
config string
173-
debug int
174-
help bool
175-
version bool
176-
all bool
177-
init bool
178-
upgrade bool
179-
}
180-
181-
// We can not parse --config and --debug flags using cobra.Command.ParseFlags
182-
//
183-
// until we read config and initialize all subcommands.
184-
// Otherwise root command will parse all flags gready.
185-
//
186-
// For example in 'lets --config lets.my.yaml mysubcommand --config=myconfig'
187-
//
188-
// cobra will parse all --config flags, but take only latest
189-
//
190-
// --config=myconfig, and this is wrong.
191-
func parseRootFlags(args []string) (*flags, error) {
192-
f := &flags{}
193-
// if first arg is not a flag, then it is subcommand
194-
if len(args) > 0 && !strings.HasPrefix(args[0], "-") {
195-
return f, nil
196-
}
197-
198-
visited := set.NewSet[string]()
199-
200-
isFlagVisited := func(name string) bool {
201-
if visited.Contains(name) {
202-
return true
203-
}
204-
205-
visited.Add(name)
206-
207-
return false
208-
}
209-
210-
idx := 0
211-
for idx < len(args) {
212-
arg := args[idx]
213-
if !strings.HasPrefix(arg, "-") {
214-
// stop if arg is not a flag, it is probably a subcommand
215-
break
216-
}
217-
218-
name, value, found := strings.Cut(arg, "=")
219-
switch name {
220-
case "--config", "-c":
221-
if !isFlagVisited("config") {
222-
if found {
223-
if value == "" {
224-
return nil, errors.New("--config must be set to value")
225-
}
226-
227-
f.config = value
228-
} else if len(args[idx:]) > 0 {
229-
f.config = args[idx+1]
230-
idx += 2
231-
232-
continue
233-
}
234-
}
235-
case "--debug", "-d", "-dd":
236-
if !isFlagVisited("debug") {
237-
f.debug = 1
238-
if arg == "-dd" {
239-
f.debug = 2
240-
}
241-
}
242-
case "--help", "-h":
243-
if !isFlagVisited("help") {
244-
f.help = true
245-
}
246-
case "--version", "-v":
247-
if !isFlagVisited("version") {
248-
f.version = true
249-
}
250-
case "--all":
251-
if !isFlagVisited("all") {
252-
f.all = true
253-
}
254-
case "--init":
255-
if !isFlagVisited("init") {
256-
f.init = true
257-
}
258-
case "--upgrade":
259-
if !isFlagVisited("upgrade") {
260-
f.upgrade = true
261-
}
262-
}
263-
264-
idx += 1 //nolint:revive,golint
265-
}
266-
267-
return f, nil
13+
os.Exit(cli.Main(Version, BuildDate))
26814
}

docs/docs/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ title: Changelog
1313
* `[Added]` When a command or its `depends` chain fails, print an indented tree to stderr showing the full chain with the failing command highlighted
1414
* `[Added]` Support `env_file` in global config and commands. File names are expanded after `env` is resolved, and values loaded from env files override values from `env`.
1515
* `[Changed]` Migrate the LSP YAML parser from the CGO-based tree-sitter bindings to pure-Go [`gotreesitter`](https://github.com/odvcencio/gotreesitter), removing the C toolchain requirement from normal builds and release packaging.
16+
* `[Refactoring]` Move CLI startup flow from `cmd/lets/main.go` into `internal/cli/cli.go`, keeping `main.go` as a thin launcher.
1617

1718
## [0.0.59](https://github.com/lets-cli/lets/releases/tag/v0.0.59)
1819

0 commit comments

Comments
 (0)