first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

395
cli/internal/app/app.go Normal file
View File

@@ -0,0 +1,395 @@
package app
import (
"context"
"errors"
"fmt"
"io"
"os"
"strings"
"sync"
"time"
"github.com/maximhq/bifrost/cli/internal/apis"
"github.com/maximhq/bifrost/cli/internal/config"
"github.com/maximhq/bifrost/cli/internal/harness"
"github.com/maximhq/bifrost/cli/internal/installer"
"github.com/maximhq/bifrost/cli/internal/mcp"
"github.com/maximhq/bifrost/cli/internal/runtime"
"github.com/maximhq/bifrost/cli/internal/secrets"
"github.com/maximhq/bifrost/cli/internal/ui/logo"
"github.com/maximhq/bifrost/cli/internal/ui/tui"
"github.com/maximhq/bifrost/cli/internal/update"
"golang.org/x/term"
)
// Options holds the CLI flags and build metadata passed to the application.
type Options struct {
Version string
Commit string
NoResume bool
Config string
Worktree string
}
// App is the main Bifrost CLI application. It manages configuration, state,
// and the interactive TUI loop for selecting and launching harnesses.
type App struct {
in io.Reader
out io.Writer
errOut io.Writer
opts Options
apiClient *apis.Client
state *config.State
cfgFile *config.FileConfig
statePath string
configPath string
configSource string
bootHeader string
}
// New creates a new App instance with the given I/O streams and options.
func New(in io.Reader, out, errOut io.Writer, opts Options) *App {
return &App{
in: in,
out: out,
errOut: errOut,
opts: opts,
apiClient: apis.NewClient(),
}
}
// Run starts the interactive TUI loop. It loads config and state, then presents
// the chooser, launches harnesses in a tabbed multiplexer, and loops back when
// all tabs are closed.
func (a *App) Run(ctx context.Context) error {
if err := a.loadStateAndConfig(); err != nil {
return err
}
updateCh := update.CheckInBackground(a.opts.Version, a.statePath)
activeProfile := a.getOrCreateProfile()
if activeProfile == nil {
return errors.New("failed to initialize profile")
}
vk, err := secrets.GetVirtualKey(activeProfile.ID)
if err != nil {
fmt.Fprintf(a.errOut, "warning: %v\n", err)
}
if vk == "" && a.cfgFile != nil && strings.TrimSpace(a.cfgFile.VirtualKey) != "" {
if err := secrets.SetVirtualKey(activeProfile.ID, strings.TrimSpace(a.cfgFile.VirtualKey)); err == nil {
vk = strings.TrimSpace(a.cfgFile.VirtualKey)
a.cfgFile.VirtualKey = ""
if a.configPath != "" {
if err := config.SaveConfig(a.configPath, a.cfgFile); err != nil {
fmt.Fprintf(a.errOut, "warning: save config after key migration: %v\n", err)
}
}
} else {
fmt.Fprintf(a.errOut, "warning: %v\n", err)
}
}
selection := a.state.Selections[activeProfile.ID]
if a.opts.NoResume {
selection = config.Selection{}
}
// Seed defaults from config if state has no selection
if a.cfgFile != nil {
if selection.Harness == "" {
selection.Harness = strings.TrimSpace(a.cfgFile.DefaultHarness)
}
if selection.Model == "" {
selection.Model = strings.TrimSpace(a.cfgFile.DefaultModel)
}
}
worktree := strings.TrimSpace(a.opts.Worktree)
var updateVersion string
// chooseAndPrepare runs the chooser TUI, handles installation flows,
// persists state, and returns a launch spec. Loops internally until
// the user picks a valid harness or quits.
chooseAndPrepare := func(_ context.Context, notify func(runtime.TabNoticeLevel, string), tabBarLine func() string, stdinReader io.Reader, msg string, isAfterSession bool, seed *runtime.LaunchSpec) (*runtime.LaunchSpec, error) {
seedApplied := false
for {
harnesses := a.harnessOptions()
baseURL := activeProfile.BaseURL
currentVK := vk
currentSelection := selection
currentWorktree := worktree
if seed != nil && !seedApplied {
baseURL = seed.BaseURL
currentVK = seed.VirtualKey
currentSelection.Harness = seed.Harness.ID
currentSelection.Model = seed.Model
currentWorktree = seed.Worktree
seedApplied = true
}
choice, err := tui.RunChooser(tui.ChooserConfig{
Version: a.opts.Version,
Commit: a.opts.Commit,
ConfigSrc: a.configSource,
Message: msg,
UpdateVersion: updateVersion,
BaseURL: baseURL,
VirtualKey: currentVK,
Harness: currentSelection.Harness,
Model: currentSelection.Model,
Worktree: currentWorktree,
AfterSession: isAfterSession,
ReservedRows: 1, // bottom tab bar
Harnesses: harnesses,
TabBarLine: tabBarLine,
FetchModels: a.apiClient.ListModels,
Input: stdinReader,
Notify: func(message string, isError bool) {
level := runtime.TabNoticeInfo
if isError {
level = runtime.TabNoticeError
}
if notify != nil {
notify(level, message)
}
},
})
if err != nil {
return nil, err
}
if choice.BackToTabs {
return nil, runtime.ErrBackToTabs
}
if choice.UpdateRequested {
return nil, runtime.ErrUpdateRequested
}
if choice.Quit {
return nil, nil
}
activeProfile.BaseURL = strings.TrimSpace(choice.BaseURL)
selection.Harness = strings.TrimSpace(choice.Harness)
selection.Model = strings.TrimSpace(choice.Model)
vk = strings.TrimSpace(choice.VirtualKey)
worktree = strings.TrimSpace(choice.Worktree)
h, ok := harness.Get(selection.Harness)
if !ok {
msg = "invalid harness selected"
isAfterSession = false
continue
}
// Handle install request
if choice.InstallHarness {
cmd, args := installer.InstallCommand(h)
shouldInstall, err := tui.RunConfirmInstall(a.bootHeader, h.Label, cmd+" "+strings.Join(args, " "))
if err != nil {
return nil, err
}
if !shouldInstall {
msg = h.Label + " installation skipped"
continue
}
if err := installer.EnsureNPM(); err != nil {
msg = err.Error()
continue
}
fmt.Fprintf(a.out, "\nInstalling %s...\n", h.Label)
if err := installer.RunInstall(ctx, a.out, a.errOut, h); err != nil {
msg = err.Error()
continue
}
if !installer.IsInstalled(h) {
msg = h.Label + " installed but binary still not in PATH"
continue
}
msg = h.Label + " installed successfully"
continue
}
// Save virtual key
if err := secrets.SetVirtualKey(activeProfile.ID, vk); err != nil {
fmt.Fprintf(a.errOut, "warning: %v\n", err)
}
// Persist state
a.state.LastProfileID = activeProfile.ID
a.state.Selections[activeProfile.ID] = selection
if err := config.SaveState(a.statePath, a.state); err != nil {
fmt.Fprintf(a.errOut, "warning: %v\n", err)
}
// Persist config
if a.cfgFile == nil {
a.cfgFile = &config.FileConfig{}
}
a.cfgFile.BaseURL = activeProfile.BaseURL
a.cfgFile.DefaultHarness = selection.Harness
a.cfgFile.DefaultModel = selection.Model
if a.configPath != "" {
if err := config.SaveConfig(a.configPath, a.cfgFile); err != nil {
fmt.Fprintf(a.errOut, "warning: save config: %v\n", err)
}
}
mcp.AttachBestEffort(ctx, a.out, a.errOut, h, activeProfile.BaseURL, vk)
return &runtime.LaunchSpec{
Harness: h,
BaseURL: activeProfile.BaseURL,
VirtualKey: vk,
Model: selection.Model,
Worktree: worktree,
}, nil
}
}
// Main loop — each iteration enters tabbed mode (Home → chooser → tabs).
// When all tabs close, we loop back.
message := ""
afterSession := false
// Wait for update check to complete (up to 4s — the HTTP request has a 3s timeout).
select {
case result := <-updateCh:
if result != nil && result.UpdateAvailable {
updateVersion = result.LatestVersion
a.state.LastVersionCheck = result.CheckedAt
a.state.LastKnownVersion = result.LatestVersion
_ = config.SaveState(a.statePath, a.state) // best-effort
}
updateCh = nil
case <-time.After(4 * time.Second):
}
// Enter tabbed mode — draws chrome, opens chooser, runs tabs.
err = runtime.RunTabbed(ctx, a.out, a.errOut, a.opts.Version, updateVersion, func(tabCtx context.Context, notify func(runtime.TabNoticeLevel, string), tabBarLine func() string, stdinReader io.Reader, seed *runtime.LaunchSpec) (*runtime.LaunchSpec, error) {
return chooseAndPrepare(tabCtx, notify, tabBarLine, stdinReader, message, afterSession, seed)
})
if errors.Is(err, runtime.ErrUpdateRequested) {
if err := update.RunSelfUpdate(a.opts.Version); err != nil {
return fmt.Errorf("update failed: %w", err)
}
// Re-exec with the updated binary.
execPath, err := os.Executable()
if err != nil {
fmt.Fprintf(a.out, "Updated successfully. Please restart bifrost.\n")
return nil
}
return reexecSelf(execPath, os.Args, os.Environ())
}
if errors.Is(err, runtime.ErrQuit) {
return nil
}
return err
}
// loadStateAndConfig loads configuration from saved state from the last run
func (a *App) loadStateAndConfig() error {
statePath, err := config.DefaultStatePath()
if err != nil {
return err
}
a.statePath = statePath
s, err := config.LoadState(statePath)
if err != nil {
return err
}
a.state = s
cfgPath := strings.TrimSpace(a.opts.Config)
if cfgPath == "" {
p, err := config.DefaultConfigPath()
if err == nil {
cfgPath = p
}
}
if cfgPath != "" {
cfg, source, err := config.LoadFile(cfgPath)
if err != nil {
return err
}
a.cfgFile = cfg
a.configPath = cfgPath
if source != "" {
a.configSource = source
}
}
if a.configPath == "" {
if p, err := config.DefaultConfigPath(); err == nil {
a.configPath = p
}
}
if a.configSource == "" {
a.configSource = "none"
}
width := 120
if w, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil {
width = w
}
noColor := strings.TrimSpace(os.Getenv("NO_COLOR")) != ""
a.bootHeader = logo.BootHeader(width, a.opts.Version, a.opts.Commit, a.configSource, noColor)
return nil
}
// getOrCreateProfile fetches or creates a new Bifrost CLI profile
func (a *App) getOrCreateProfile() *config.Profile {
if !a.opts.NoResume && strings.TrimSpace(a.state.LastProfileID) != "" {
if p := a.state.ProfileByID(a.state.LastProfileID); p != nil {
return p
}
}
if len(a.state.Profiles) > 0 && !a.opts.NoResume {
return &a.state.Profiles[0]
}
p := config.Profile{ID: "default", Name: "Default"}
if a.cfgFile != nil {
p.BaseURL = strings.TrimSpace(a.cfgFile.BaseURL)
}
if existing := a.state.ProfileByID("default"); existing != nil {
if strings.TrimSpace(existing.BaseURL) == "" {
existing.BaseURL = p.BaseURL
}
return existing
}
a.state.Profiles = append(a.state.Profiles, p)
return &a.state.Profiles[len(a.state.Profiles)-1]
}
// harnessOptions responds with available harness options with states like installed/not installed etc.
// Version detection runs concurrently across all harnesses to avoid serial subprocess waits.
func (a *App) harnessOptions() []tui.HarnessOption {
ids := harness.IDs()
out := make([]tui.HarnessOption, len(ids))
var wg sync.WaitGroup
for i, id := range ids {
h, _ := harness.Get(id)
out[i] = tui.HarnessOption{
ID: h.ID,
Label: h.Label,
SupportsWorktree: h.SupportsWorktree,
SupportsModelOverride: h.RunArgsForMod != nil || h.ModelEnv != "" || h.PreLaunch != nil,
}
wg.Add(1)
go func(idx int, h harness.Harness) {
defer wg.Done()
out[idx].Installed = installer.IsInstalled(h)
out[idx].Version = harness.DetectVersion(h)
}(i, h)
}
wg.Wait()
return out
}