internal/cli/cli.go
1
// Package cli implements the koment commands a human or a shell hook runs.
2
package cli
4
import (
5
"flag"
6
"fmt"
7
"io"
8
"os"
9
"strings"
11
"github.com/janpuc/koment/internal/application"
12
repositorymodel "github.com/janpuc/koment/internal/repository"
13
"github.com/janpuc/koment/internal/store"
14
)
16
const (
17
ExitOK = 0
18
ExitFailure = 1
19
ExitUsage = 2
20
)
22
const usage = `koment — out-of-band code annotations
24
koment add <file> [--excerpt <text>] --kind <kind> --body <text|->
25
koment show <file>
26
koment check [path...]
27
koment list [--kind <kind>]
28
koment search <query>
29
koment reanchor <id> [--excerpt <text>] [--file <path>]
30
koment comments check [path...]
31
koment comments convert <file> --excerpt <comment> [--kind <kind>]
32
koment comments acknowledge <file> --excerpt <comment> --body <text|-> --acknowledge-inline-comment
33
koment agents install|check
34
koment ui [--listen <addr>] [--write]
35
koment serve --config <repositories.yaml>
36
koment site --out <dir> render one repository to static HTML
37
koment mcp [--write | --http <addr> | --streamable-http <addr>]
38
koment lsp editor protocol over stdio
39
koment version
41
check exits non-zero when an annotation is ambiguous, drifted or orphaned.
42
reanchor is how you fix one while keeping its id.
43
`
45
type Environment struct {
46
Stdin io.Reader
47
Stdout io.Writer
48
Stderr io.Writer
49
Build Build
50
}
52
// Server runs a long-lived server, parsing its own flags.
53
type Server func(args []string, stderr io.Writer) error
55
// Servers are injected rather than imported. Adding one is a new field, not a
56
// new parameter, so signatures here stop changing shape.
57
type Servers struct {
58
MCP Server
59
UI Server
60
Site Server
61
Serve Server
62
LSP Server
63
}
65
// Run dispatches a subcommand.
66
func Run(args []string, env Environment, servers Servers) int {
67
if len(args) == 0 {
68
fmt.Fprint(env.Stderr, usage)
69
return ExitUsage
70
}
72
command, rest := args[0], args[1:]
73
run, known := map[string]func([]string, Environment) int{
74
"add": runAdd,
75
"agents": runAgents,
76
"show": runShow,
77
"check": runCheck,
78
"comments": runComments,
79
"list": runList,
80
"search": runSearch,
81
"reanchor": runReanchor,
82
"version": runVersion,
83
}[command]
85
switch {
86
case known:
87
return run(rest, env)
88
case command == "mcp":
89
if err := servers.MCP(rest, env.Stderr); err != nil {
90
return fail(env, err)
91
}
92
return ExitOK
93
case command == "ui":
94
if err := servers.UI(rest, env.Stderr); err != nil {
95
return fail(env, err)
96
}
97
return ExitOK
98
case command == "site":
99
if err := servers.Site(rest, env.Stderr); err != nil {
100
return fail(env, err)
101
}
102
return ExitOK
103
case command == "serve":
104
if err := servers.Serve(rest, env.Stderr); err != nil {
105
return fail(env, err)
106
}
107
return ExitOK
108
case command == "lsp":
109
if err := servers.LSP(rest, env.Stderr); err != nil {
110
return fail(env, err)
111
}
112
return ExitOK
113
case command == "help", command == "-h", command == "--help":
114
fmt.Fprint(env.Stdout, usage)
115
return ExitOK
116
}
118
fmt.Fprintf(env.Stderr, "koment: unknown command %q\n\n%s", command, usage)
119
return ExitUsage
120
}
122
func fail(env Environment, err error) int {
123
fmt.Fprintf(env.Stderr, "koment: %v\n", err)
124
return ExitFailure
125
}
127
func misuse(env Environment, format string, args ...any) int {
128
fmt.Fprintf(env.Stderr, "koment: "+format+"\n", args...)
129
return ExitUsage
130
}
132
func flagSet(name string, env Environment) *flag.FlagSet {
133
flags := flag.NewFlagSet(name, flag.ContinueOnError)
134
flags.SetOutput(env.Stderr)
135
return flags
136
}
138
func onePositional(command, what string, flags *flag.FlagSet, args []string, env Environment) (string, bool) {
139
value, rest := leadingNonFlag(args)
140
if err := flags.Parse(rest); err != nil {
141
return "", false
142
}
144
switch {
145
case value == "":
146
value = flags.Arg(0)
147
case flags.NArg() > 0:
148
misuse(env, "%s takes one %s, also got %s", command, what, strings.Join(flags.Args(), " "))
149
return "", false
150
}
152
if value == "" {
153
misuse(env, "%s needs %s", command, what)
154
return "", false
155
}
156
return value, true
157
}
159
func leadingNonFlag(args []string) (string, []string) {
160
if len(args) > 0 && !strings.HasPrefix(args[0], "-") {
161
return args[0], args[1:]
162
}
163
return "", args
164
}
166
func openStore() (*store.Store, error) {
167
workingDirectory, err := os.Getwd()
168
if err != nil {
169
return nil, fmt.Errorf("finding the working directory: %w", err)
170
}
171
root, err := store.FindRoot(workingDirectory)
172
if err != nil {
173
return nil, err
174
}
175
return store.Open(root), nil
176
}
178
func openApplication() (*application.Service, *store.Store, error) {
179
annotations, err := openStore()
180
if err != nil {
181
return nil, nil, err
182
}
183
entry := repositorymodel.Repository{ID: "local", Name: "Local repository", Root: annotations.Root()}
184
return application.NewService(entry), annotations, nil
185
}