-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdispatch.go
More file actions
218 lines (197 loc) · 5.89 KB
/
Copy pathdispatch.go
File metadata and controls
218 lines (197 loc) · 5.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
package main
import (
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"log"
"os"
"path/filepath"
"runtime/debug"
"sort"
"strings"
term "github.com/charmbracelet/x/term"
"github.com/RandomCodeSpace/kb/internal/mcpserv"
)
// subcommands is the kb subcommand dispatch table. A bare invocation is
// handled separately because it depends on whether stdin and stdout are TTYs.
var subcommands = map[string]func(args []string) error{
"mcp": runMCP,
"serve": runWebServer,
"tui": runTUI,
"version": runVersion,
}
var mcpRun = mcpserv.Run
var exitProcess = os.Exit
var fatalLogf = log.Fatalf
var (
rootStdout io.Writer = os.Stdout
rootStderr io.Writer = os.Stderr
stdinIsTerminal = func() bool { return term.IsTerminal(os.Stdin.Fd()) }
stdoutIsTerminal = func() bool { return term.IsTerminal(os.Stdout.Fd()) }
)
type rootUsageError struct{ message string }
func (e *rootUsageError) Error() string { return e.message }
// runRoot handles only the commandless surface. It never opens the data store
// when either standard stream is non-interactive.
func runRoot(args []string) error {
if len(args) != 0 {
if len(args) == 1 && (args[0] == "-h" || args[0] == "--help") {
fmt.Fprint(rootStdout, rootUsageText)
return nil
}
return &rootUsageError{message: "root flags are no longer accepted; use `kb serve --port ... --data ... --log ...` for the optional API server"}
}
if !stdinIsTerminal() || !stdoutIsTerminal() {
fmt.Fprint(rootStdout, rootUsageText)
return nil
}
return runTUI(nil)
}
// dispatch runs os.Args[1] as a subcommand when one is named, reporting
// whether it handled the invocation. Unknown subcommands exit with an error
// so typos never silently start the web server.
func dispatch() bool {
handled, code := dispatchArgs(os.Args[1:], os.Stderr)
if code != 0 {
exitProcess(code)
}
return handled
}
func dispatchArgs(args []string, stderr io.Writer) (handled bool, exitCode int) {
if len(args) == 0 || strings.HasPrefix(args[0], "-") {
return false, 0
}
cmd := args[0]
fn, ok := subcommands[cmd]
if !ok {
names := make([]string, 0, len(subcommands))
for name := range subcommands {
names = append(names, name)
}
sort.Strings(names)
fmt.Fprintf(stderr, "kb: unknown command %q (known: %s)\n", cmd, strings.Join(names, ", "))
return true, 2
}
if err := fn(args[1:]); err != nil {
var flagErr *webFlagError
if errors.As(err, &flagErr) {
if errors.Is(flagErr, flag.ErrHelp) {
return true, 0
}
fmt.Fprintf(stderr, "kb %s: %v\n", cmd, err)
return true, 2
}
fmt.Fprintf(stderr, "kb %s: %v\n", cmd, err)
return true, 1
}
return true, 0
}
var readBuildInfo = debug.ReadBuildInfo
var versionOut io.Writer = os.Stdout
// runVersion prints the build's version: kb version [--json]. go-install
// builds carry the module version; release binaries are built at the tagged
// commit with a clean tree, so the toolchain stamps the same value there. A
// plain go build in a checkout reports devel plus the commit it was built
// from.
func runVersion(args []string) error {
fs := flag.NewFlagSet("kb version", flag.ContinueOnError)
fs.SetOutput(io.Discard)
jsonOut := fs.Bool("json", false, "print the version as JSON")
if err := fs.Parse(args); err != nil {
return err
}
if fs.NArg() > 0 {
return fmt.Errorf("version takes no arguments")
}
info, ok := readBuildInfo()
if *jsonOut {
encoded, err := json.MarshalIndent(versionJSON(info, ok), "", " ")
if err != nil {
return err
}
fmt.Fprintln(versionOut, string(encoded))
return nil
}
fmt.Fprintln(versionOut, versionString(info, ok))
return nil
}
// versionParts extracts the display version, the 12-char revision, and the
// dirty flag from build info. version is "unknown" without build info and
// "devel" for unstamped builds.
func versionParts(info *debug.BuildInfo, ok bool) (version, revision string, modified bool) {
if !ok || info == nil {
return "unknown", "", false
}
version = info.Main.Version
if version == "" || version == "(devel)" {
version = "devel"
}
for _, s := range info.Settings {
switch s.Key {
case "vcs.revision":
revision = s.Value
case "vcs.modified":
modified = s.Value == "true"
}
}
if len(revision) > 12 {
revision = revision[:12]
}
return version, revision, modified
}
type versionInfoJSON struct {
Version string `json:"version"`
Revision string `json:"revision,omitempty"`
Modified bool `json:"modified,omitempty"`
}
func versionJSON(info *debug.BuildInfo, ok bool) versionInfoJSON {
version, revision, modified := versionParts(info, ok)
return versionInfoJSON{Version: version, Revision: revision, Modified: modified}
}
func versionString(info *debug.BuildInfo, ok bool) string {
version, revision, modified := versionParts(info, ok)
if version == "unknown" {
return "kb (unknown build)"
}
out := "kb " + version
if revision != "" {
out += " (" + revision
if modified {
out += ", modified"
}
out += ")"
}
return out
}
// runMCP serves the board over MCP stdio: kb mcp [--data DIR] [--user NAME].
func runMCP(args []string) error {
fs := flag.NewFlagSet("kb mcp", flag.ExitOnError)
dataDir := fs.String("data", defaultDataDir(), "board storage directory (env KB_DATA)")
user := fs.String("user", envOr("KB_USER", "default"), "board user the tools operate on (env KB_USER)")
if err := fs.Parse(args); err != nil {
return err
}
return mcpRun(*dataDir, *user)
}
// defaultDataDir resolves the board storage directory: KB_DATA if set, else
// ~/.local/share/kb.
func defaultDataDir() string {
dir, err := resolveDefaultDataDir()
if err != nil {
fatalLogf("cannot determine home directory, set KB_DATA or --data: %v", err)
}
return dir
}
var userHomeDir = os.UserHomeDir
func resolveDefaultDataDir() (string, error) {
if v := os.Getenv("KB_DATA"); v != "" {
return v, nil
}
home, err := userHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, ".local", "share", "kb"), nil
}