internal/cli/cli.go
1
// Package cli implements the koment commands a human or a shell hook runs.
2
package cli
4
import (
5
"errors"
6
"flag"
7
"fmt"
8
"io"
9
"os"
10
"strings"
12
"github.com/koment-dev/koment/internal/application"
13
"github.com/koment-dev/koment/internal/policy"
14
repositorymodel "github.com/koment-dev/koment/internal/repository"
15
"github.com/koment-dev/koment/internal/store"
16
)
18
const (
19
ExitOK = 0
20
ExitFailure = 1
21
ExitUsage = 2
22
)
24
type Environment struct {
25
Stdin io.Reader
26
Stdout io.Writer
27
Stderr io.Writer
28
Build Build
29
}
31
// Server runs a long-lived server, parsing its own flags.
32
type Server func(args []string, stderr io.Writer) error
34
// Servers are injected rather than imported. Adding one is a new field, not a
35
// new parameter, so signatures here stop changing shape.
36
type Servers struct {
37
MCP Server
38
UI Server
39
Site Server
40
Serve Server
41
LSP Server
42
}
44
// Run dispatches a subcommand.
45
func Run(args []string, env Environment, servers Servers) int {
46
if len(args) == 0 {
47
writeUsage(env.Stderr)
48
return ExitUsage
49
}
51
command, rest := args[0], args[1:]
52
run, known := map[string]func([]string, Environment) int{
53
"add": runAdd,
54
"agents": runAgents,
55
"bootstrap": runBootstrap,
56
"show": runShow,
57
"check": runCheck,
58
"comments": runComments,
59
"edit": runEdit,
60
"forget": runForget,
61
"list": runList,
62
"search": runSearch,
63
"reanchor": runReanchor,
64
"version": runVersion,
65
}[command]
67
switch {
68
case known:
69
return run(rest, env)
70
case command == "mcp":
71
if err := servers.MCP(rest, env.Stderr); err != nil {
72
return fail(env, err)
73
}
74
return ExitOK
75
case command == "ui":
76
if err := servers.UI(rest, env.Stderr); err != nil {
77
return fail(env, err)
78
}
79
return ExitOK
80
case command == "site":
81
if err := servers.Site(rest, env.Stderr); err != nil {
82
return fail(env, err)
83
}
84
return ExitOK
85
case command == "serve":
86
if err := servers.Serve(rest, env.Stderr); err != nil {
87
return fail(env, err)
88
}
89
return ExitOK
90
case command == "lsp":
91
if err := servers.LSP(rest, env.Stderr); err != nil {
92
return fail(env, err)
93
}
94
return ExitOK
95
case command == "help", command == "-h", command == "--help":
96
writeUsage(env.Stdout)
97
return ExitOK
98
}
100
fmt.Fprintf(env.Stderr, "koment: unknown command %q\n\n", command)
101
writeUsage(env.Stderr)
102
return ExitUsage
103
}
105
func fail(env Environment, err error) int {
106
fmt.Fprintf(env.Stderr, "koment: %v\n", err)
107
return ExitFailure
108
}
110
func misuse(env Environment, format string, args ...any) int {
111
fmt.Fprintf(env.Stderr, "koment: "+format+"\n", args...)
112
return ExitUsage
113
}
115
func flagSet(name string, env Environment) *flag.FlagSet {
116
flags := flag.NewFlagSet(name, flag.ContinueOnError)
117
flags.SetOutput(env.Stderr)
118
flags.Usage = func() { writeCommandUsage(env.Stderr, name, flags) }
119
return flags
120
}
122
func parse(flags *flag.FlagSet, args []string) (int, bool) {
123
err := flags.Parse(args)
124
switch {
125
case err == nil:
126
return ExitOK, true
127
case errors.Is(err, flag.ErrHelp):
128
return ExitOK, false
129
default:
130
return ExitUsage, false
131
}
132
}
134
func onePositional(command, what string, flags *flag.FlagSet, args []string, env Environment) (string, int, bool) {
135
value, rest := leadingNonFlag(args)
136
if code, ok := parse(flags, rest); !ok {
137
return "", code, false
138
}
140
switch {
141
case value == "":
142
value = flags.Arg(0)
143
case flags.NArg() > 0:
144
misuse(env, "%s takes one %s, also got %s", command, what, strings.Join(flags.Args(), " "))
145
return "", ExitUsage, false
146
}
148
if value == "" {
149
misuse(env, "%s needs %s", command, what)
150
return "", ExitUsage, false
151
}
152
return value, ExitOK, true
153
}
155
func leadingNonFlag(args []string) (string, []string) {
156
if len(args) > 0 && !strings.HasPrefix(args[0], "-") {
157
return args[0], args[1:]
158
}
159
return "", args
160
}
162
func openStore() (*store.Store, error) {
163
workingDirectory, err := os.Getwd()
164
if err != nil {
165
return nil, fmt.Errorf("finding the working directory: %w", err)
166
}
167
root, err := store.FindRoot(workingDirectory)
168
if err != nil {
169
return nil, err
170
}
171
return store.Open(root), nil
172
}
174
func openApplication() (*application.Service, *store.Store, error) {
175
annotations, err := openStore()
176
if err != nil {
177
return nil, nil, err
178
}
179
entry := repositorymodel.Repository{ID: "local", Name: "Local repository", Root: annotations.Root()}
180
return application.NewService(entry), annotations, nil
181
}
183
type activeRepository struct {
184
service *application.Service
185
annotations *store.Store
186
configured policy.Policy
187
}
189
func openActiveRepository() (*activeRepository, error) {
190
workingDirectory, err := os.Getwd()
191
if err != nil {
192
return nil, fmt.Errorf("finding the working directory: %w", err)
193
}
194
activation, err := policy.Detect(workingDirectory)
195
if err != nil {
196
return nil, err
197
}
198
if activation == nil {
199
return nil, nil
200
}
201
annotations := store.Open(activation.Root)
202
entry := repositorymodel.Repository{ID: "local", Name: "Local repository", Root: activation.Root}
203
return &activeRepository{
204
service: application.NewService(entry),
205
annotations: annotations,
206
configured: activation.Configured,
207
}, nil
208
}