1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5package config
6
7import (
8 "errors"
9 "fmt"
10 "strconv"
11
12 "github.com/charmbracelet/huh"
13 "github.com/spf13/cobra"
14
15 "git.secluded.site/lunatask-mcp-server/internal/config"
16 "git.secluded.site/lunatask-mcp-server/internal/ui"
17)
18
19// Port validation errors.
20var (
21 errPortNotNumber = errors.New("port must be a number")
22 errPortOutOfRange = errors.New("port must be between 1 and 65535")
23)
24
25// runServerStep runs the server configuration step.
26//
27//nolint:cyclop,funlen // wizard flow benefits from being in one function
28func runServerStep(cfg *config.Config) wizardNav {
29 if cfg.Server.Host == "" {
30 cfg.Server.Host = "localhost"
31 }
32
33 if cfg.Server.Port == 0 {
34 cfg.Server.Port = 8080
35 }
36
37 if cfg.Server.Transport == "" {
38 cfg.Server.Transport = "stdio"
39 }
40
41 if cfg.Timezone == "" {
42 cfg.Timezone = "UTC"
43 }
44
45 err := huh.NewSelect[string]().
46 Title("Transport mode").
47 Description("How will clients connect to the server?").
48 Options(
49 huh.NewOption("stdio (for CLI tools like Crush, Claude Code)", "stdio"),
50 huh.NewOption("SSE (for Home Assistant)", "sse"),
51 huh.NewOption("HTTP (streamable HTTP transport)", "http"),
52 ).
53 Value(&cfg.Server.Transport).
54 Run()
55 if err != nil {
56 if errors.Is(err, huh.ErrUserAborted) {
57 return navQuit
58 }
59
60 return navQuit
61 }
62
63 // Only ask for host/port if not using stdio
64 if cfg.Server.Transport != "stdio" {
65 portStr := strconv.Itoa(cfg.Server.Port)
66
67 err = huh.NewForm(
68 huh.NewGroup(
69 huh.NewInput().
70 Title("Host").
71 Description("Address to listen on.").
72 Placeholder("localhost").
73 Value(&cfg.Server.Host),
74 huh.NewInput().
75 Title("Port").
76 Description("Port to listen on.").
77 Placeholder("8080").
78 Value(&portStr).
79 Validate(validatePort),
80 ),
81 ).Run()
82 if err != nil {
83 if errors.Is(err, huh.ErrUserAborted) {
84 return navQuit
85 }
86
87 return navQuit
88 }
89
90 if portStr != "" {
91 cfg.Server.Port, _ = strconv.Atoi(portStr)
92 }
93 }
94
95 err = huh.NewInput().
96 Title("Timezone").
97 Description("IANA timezone for date parsing (e.g. America/New_York, Europe/London).").
98 Placeholder("UTC").
99 Value(&cfg.Timezone).
100 Run()
101 if err != nil {
102 if errors.Is(err, huh.ErrUserAborted) {
103 return navQuit
104 }
105
106 return navQuit
107 }
108
109 return navNext
110}
111
112// runAreasStep runs the areas configuration step.
113func runAreasStep(cfg *config.Config) wizardNav {
114 return manageAreasAsStep(cfg)
115}
116
117// runHabitsStep runs the habits configuration step.
118func runHabitsStep(cfg *config.Config) wizardNav {
119 return manageHabitsAsStep(cfg)
120}
121
122// runAccessTokenStep runs the access token configuration step.
123func runAccessTokenStep(cmd *cobra.Command) wizardNav {
124 err := configureAccessToken(cmd)
125 if errors.Is(err, errQuit) {
126 return navQuit
127 }
128
129 if err != nil {
130 out := cmd.OutOrStdout()
131 _, _ = fmt.Fprintln(out, ui.Error.Render("Token configuration failed: "+err.Error()))
132
133 var skip bool
134
135 skipErr := huh.NewConfirm().
136 Title("Skip token configuration?").
137 Description("You can configure it later with 'lunatask-mcp-server config'.").
138 Affirmative("Skip for now").
139 Negative("Go back").
140 Value(&skip).
141 Run()
142 if skipErr != nil {
143 return navQuit
144 }
145
146 if skip {
147 return navNext
148 }
149
150 return navBack
151 }
152
153 return navNext
154}
155
156// configureServer runs the server configuration from the main menu.
157func configureServer(cfg *config.Config) error {
158 nav := runServerStep(cfg)
159 if nav == navQuit {
160 return errQuit
161 }
162
163 return nil
164}
165
166func validatePort(s string) error {
167 if s == "" {
168 return nil
169 }
170
171 p, err := strconv.Atoi(s)
172 if err != nil {
173 return errPortNotNumber
174 }
175
176 if p < 1 || p > 65535 {
177 return errPortOutOfRange
178 }
179
180 return nil
181}