Skip to content

Commit d56d3cd

Browse files
committed
fix: add missing cmd/bodek entry point
- Create the CLI main package referenced by README/Makefile/.goreleaser - Anchor the /bodek ignore pattern so cmd/bodek sources are tracked - Make TestSpawnDefaultBinMissing deterministic by isolating PATH
1 parent c8f4324 commit d56d3cd

3 files changed

Lines changed: 103 additions & 1 deletion

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Build artifacts
22
/bin/
3-
bodek
3+
/bodek
44

55
# Editor / OS noise
66
.DS_Store

cmd/bodek/main.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Command bodek is a beautiful terminal interface for the odek agent.
2+
// It launches (or attaches to) an odek serve instance and renders the agent's
3+
// live stream as a Bubble Tea TUI.
4+
package main
5+
6+
import (
7+
"flag"
8+
"fmt"
9+
"os"
10+
"os/signal"
11+
"syscall"
12+
13+
tea "github.com/charmbracelet/bubbletea"
14+
15+
"github.com/BackendStack21/bodek/internal/client"
16+
"github.com/BackendStack21/bodek/internal/server"
17+
"github.com/BackendStack21/bodek/internal/tui"
18+
)
19+
20+
func main() {
21+
if err := run(); err != nil {
22+
fmt.Fprintln(os.Stderr, "bodek:", err)
23+
os.Exit(1)
24+
}
25+
}
26+
27+
func run() error {
28+
var (
29+
url = flag.String("url", "", "attach to an already-running odek serve URL (e.g. http://127.0.0.1:8080)")
30+
sandbox = flag.Bool("sandbox", false, "run tool calls inside odek's Docker sandbox")
31+
bin = flag.String("odek-bin", "", "path to the odek binary to spawn (default: odek on PATH)")
32+
)
33+
flag.Usage = func() {
34+
fmt.Fprintf(os.Stderr, "Usage: bodek [options] [-- <odek serve flags>]\n\n")
35+
fmt.Fprintf(os.Stderr, "A terminal interface for the odek agent.\n\n")
36+
fmt.Fprintf(os.Stderr, "Options:\n")
37+
flag.PrintDefaults()
38+
fmt.Fprintf(os.Stderr, "\nExamples:\n")
39+
fmt.Fprintf(os.Stderr, " bodek # spawn odek serve and start chatting\n")
40+
fmt.Fprintf(os.Stderr, " bodek --sandbox # spawn odek serve with Docker sandbox\n")
41+
fmt.Fprintf(os.Stderr, " bodek --url http://127.0.0.1:8080 # attach to a running odek serve\n")
42+
fmt.Fprintf(os.Stderr, " bodek -- --prompt-caching # pass extra flags to odek serve\n")
43+
}
44+
flag.Parse()
45+
46+
extraArgs := flag.Args()
47+
48+
// Spawn or attach to the odek serve backend.
49+
srv, err := server.Connect(server.Options{
50+
URL: *url,
51+
Bin: *bin,
52+
Sandbox: *sandbox,
53+
ExtraArgs: extraArgs,
54+
Stderr: os.Stderr,
55+
})
56+
if err != nil {
57+
return err
58+
}
59+
defer srv.Stop()
60+
61+
// Dial the WebSocket and start streaming events.
62+
cl, err := client.Dial(srv.WSURL, srv.Origin, srv.BaseURL, srv.Token)
63+
if err != nil {
64+
return err
65+
}
66+
defer cl.Close()
67+
68+
cwd, err := os.Getwd()
69+
if err != nil {
70+
cwd = "."
71+
}
72+
73+
// Gracefully shutdown on SIGINT/SIGTERM so the server gets a clean exit.
74+
setupSignalHandler(srv, cl)
75+
76+
model := tui.New(cl, tui.Options{
77+
Sandbox: *sandbox,
78+
CWD: cwd,
79+
})
80+
81+
p := tea.NewProgram(model, tea.WithAltScreen(), tea.WithMouseCellMotion())
82+
if _, err := p.Run(); err != nil {
83+
return fmt.Errorf("TUI exited: %w", err)
84+
}
85+
return nil
86+
}
87+
88+
func setupSignalHandler(srv *server.Conn, cl *client.Client) {
89+
ch := make(chan os.Signal, 1)
90+
signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
91+
go func() {
92+
<-ch
93+
_ = cl.Close()
94+
srv.Stop()
95+
os.Exit(0)
96+
}()
97+
}

internal/server/server_internal_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package server
33
import (
44
"net/http"
55
"net/http/httptest"
6+
"os"
67
"os/exec"
78
"testing"
89
"time"
@@ -47,6 +48,10 @@ func TestStopInterruptsLongProcess(t *testing.T) {
4748

4849
func TestSpawnDefaultBinMissing(t *testing.T) {
4950
// Empty Bin defaults to "odek"; absent from PATH here → LookPath error.
51+
// Isolate PATH so the test is deterministic even when odek is installed.
52+
oldPath := os.Getenv("PATH")
53+
defer os.Setenv("PATH", oldPath)
54+
os.Setenv("PATH", "")
5055
c := &Conn{}
5156
if err := c.spawn(Options{Bin: ""}, "127.0.0.1:0"); err == nil {
5257
t.Error("expected default-bin lookup to fail")

0 commit comments

Comments
 (0)