snapshot of eee7b5cfc0776c68816d7fdaa8af9d7a0e0da2e6 Annotations about the code that implements koment.

internal/cli/cli.go

1 // Package cli implements the koment commands a human or a shell hook runs.
2 package cli
3
4 import (
5 "flag"
6 "fmt"
7 "io"
8 "os"
9 "strings"
10
11 "github.com/janpuc/koment/internal/application"
12 repositorymodel "github.com/janpuc/koment/internal/repository"
13 "github.com/janpuc/koment/internal/store"
14 )
15
16 const (
17 ExitOK = 0
18 ExitFailure = 1
19 ExitUsage = 2
20 )
21
22 const usage = `koment — out-of-band code annotations
23
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
40
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 `
44
45 type Environment struct {
46 Stdin io.Reader
47 Stdout io.Writer
48 Stderr io.Writer
49 Build Build
50 }
51
52 // Server runs a long-lived server, parsing its own flags.
53 type Server func(args []string, stderr io.Writer) error
54
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 }
64
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 }
71
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]
84
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 }
117
118 fmt.Fprintf(env.Stderr, "koment: unknown command %q\n\n%s", command, usage)
119 return ExitUsage
120 }
121
122 func fail(env Environment, err error) int {
123 fmt.Fprintf(env.Stderr, "koment: %v\n", err)
124 return ExitFailure
125 }
126
127 func misuse(env Environment, format string, args ...any) int {
128 fmt.Fprintf(env.Stderr, "koment: "+format+"\n", args...)
129 return ExitUsage
130 }
131
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 }
137
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 }
143
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 }
151
152 if value == "" {
153 misuse(env, "%s needs %s", command, what)
154 return "", false
155 }
156 return value, true
157 }
158
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 }
165
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 }
177
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 }

Find an annotation

Search file paths, rationale, kinds, and authors.

moveEnter openEsc close