package main
import (
"fmt"
"os"
"slices"
"strings"
"tobi/internal/app"
"tobi/internal/remote"
"tobi/internal/session"
)
func main() {
if err := dispatch(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
const usage = `tobi - persistent terminals across your devices
usage:
tobi attach to a session with the fuzzy selector
tobi <query> attach using fuzzy search (tobi hx, tobi host.id)
tobi <target> -- cmd spawn cmd in a session on target
target is [user@][host.]id and host is optional, so tobi .id -- nu creates a
local session with that id. the daemon detaches when the terminal window
closes and detached sessions are cleaned up after 24h. sessions are
reachable from any device on the tailscale network by running tobi. tobi
doesn't nest: running it inside a tobi session replaces it.
requires tssh (or ssh) and tailscale in PATH.
`
func dispatch(args []string) error {
switch op := os.Getenv("TOBI_RPC"); {
case os.Getenv("TOBI_COMPLETE") == "1":
return app.Complete(strings.Join(args, " "))
case os.Getenv("TOBI_OWNER") == "1":
id := os.Getenv("TOBI_ID")
for _, k := range []string{"TOBI_OWNER", "TOBI_ID", "TOBI_RPC", "TOBI_COMPLETE"} {
os.Unsetenv(k)
}
return wrapErr("owner", session.RunOwner(id, args))
case op != "":
return wrapErr("rpc "+op, remote.RunRPC(op, os.Getenv("TOBI_ID")))
}
if len(args) > 0 && (args[0] == "--help" || args[0] == "-h") {
fmt.Print(usage)
return nil
}
if i := slices.Index(args, "--"); i != -1 {
return app.Spawn(strings.Join(args[:i], " "), args[i+1:])
}
return app.Interactive(strings.Join(args, " "))
}
func wrapErr(prefix string, err error) error {
if err != nil {
return fmt.Errorf("%s: %w", prefix, err)
}
return nil
}