164 lines
5.0 KiB
Go
164 lines
5.0 KiB
Go
package runtime
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"github.com/maximhq/bifrost/cli/internal/apis"
|
|
"github.com/maximhq/bifrost/cli/internal/harness"
|
|
)
|
|
|
|
// LaunchSpec holds the parameters needed to launch a harness subprocess.
|
|
type LaunchSpec struct {
|
|
Harness harness.Harness
|
|
BaseURL string
|
|
VirtualKey string
|
|
Model string
|
|
Worktree string // empty = no worktree, non-empty = worktree name (or " " for unnamed)
|
|
}
|
|
|
|
// BuildEnv constructs the environment variables for the harness process,
|
|
// including the provider endpoint, API key, and model overrides.
|
|
func BuildEnv(spec LaunchSpec) ([]string, error) {
|
|
endpoint, err := apis.BuildEndpoint(spec.BaseURL, spec.Harness.BasePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
env := os.Environ()
|
|
env = append(env, spec.Harness.BaseURLEnv+"="+endpoint)
|
|
|
|
vk := strings.TrimSpace(spec.VirtualKey)
|
|
if vk != "" {
|
|
env = append(env, spec.Harness.APIKeyEnv+"="+vk)
|
|
if spec.Harness.AuthTokenEnv != "" {
|
|
env = append(env, spec.Harness.AuthTokenEnv+"="+vk)
|
|
}
|
|
}
|
|
model := strings.TrimSpace(spec.Model)
|
|
if model != "" {
|
|
env = append(env, "BIFROST_MODEL="+model)
|
|
if spec.Harness.ModelEnv != "" {
|
|
env = append(env, spec.Harness.ModelEnv+"="+model)
|
|
}
|
|
}
|
|
|
|
// Mark session as running inside bifrost
|
|
env = append(env, "BIFROST_SESSION=1")
|
|
env = append(env, "BIFROST_BASE_URL="+spec.BaseURL)
|
|
return env, nil
|
|
}
|
|
|
|
// PreparedCmd holds a command ready to execute along with any cleanup
|
|
// function that should be called after the process exits.
|
|
type PreparedCmd struct {
|
|
Cmd *exec.Cmd
|
|
Cleanup func()
|
|
}
|
|
|
|
// PrepareCommand builds the exec.Cmd for a harness launch, including
|
|
// environment variables, pre-launch hooks, and CLI arguments.
|
|
func PrepareCommand(ctx context.Context, spec LaunchSpec) (*PreparedCmd, error) {
|
|
env, err := BuildEnv(spec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var cleanup func()
|
|
if spec.Harness.PreLaunch != nil {
|
|
endpoint, err := apis.BuildEndpoint(spec.BaseURL, spec.Harness.BasePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("build endpoint for pre-launch: %w", err)
|
|
}
|
|
vk := strings.TrimSpace(spec.VirtualKey)
|
|
if vk == "" {
|
|
vk = "dummy-key"
|
|
}
|
|
extraEnv, c, err := spec.Harness.PreLaunch(endpoint, vk, spec.Model)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("pre-launch %s: %w", spec.Harness.Label, err)
|
|
}
|
|
cleanup = c
|
|
env = append(env, extraEnv...)
|
|
}
|
|
|
|
args := []string{}
|
|
if spec.Harness.RunArgsForMod != nil {
|
|
args = append(args, spec.Harness.RunArgsForMod(spec.Model)...)
|
|
}
|
|
if spec.Worktree != "" && spec.Harness.WorktreeArgs != nil {
|
|
args = append(args, spec.Harness.WorktreeArgs(spec.Worktree)...)
|
|
}
|
|
|
|
cmd := exec.CommandContext(ctx, spec.Harness.Binary, args...)
|
|
cmd.Env = env
|
|
|
|
return &PreparedCmd{Cmd: cmd, Cleanup: cleanup}, nil
|
|
}
|
|
|
|
// RunInteractive launches the harness as an interactive subprocess with full
|
|
// TTY access. It prints a bifrost banner before launch and a summary after exit.
|
|
func RunInteractive(ctx context.Context, stdout, stderr io.Writer, spec LaunchSpec) error {
|
|
p, err := PrepareCommand(ctx, spec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if p.Cleanup != nil {
|
|
defer p.Cleanup()
|
|
}
|
|
|
|
fmt.Fprint(stdout, renderBanner(spec))
|
|
|
|
if err := runWithPTY(ctx, stdout, p.Cmd); err != nil {
|
|
fmt.Fprintf(stdout, "\n\033[36mbifrost>\033[0m session ended with error: %v\n", err)
|
|
return fmt.Errorf("run harness: %w", err)
|
|
}
|
|
fmt.Fprintf(stdout, "\n\033[36mbifrost>\033[0m session ended\n")
|
|
return nil
|
|
}
|
|
|
|
// renderBanner builds the pre-launch info box showing harness, model,
|
|
// endpoint, and the equivalent command.
|
|
func renderBanner(spec LaunchSpec) string {
|
|
endpoint, err := apis.BuildEndpoint(spec.BaseURL, spec.Harness.BasePath)
|
|
if err != nil {
|
|
endpoint = spec.BaseURL + " (invalid)"
|
|
}
|
|
|
|
vkStatus := "no"
|
|
if strings.TrimSpace(spec.VirtualKey) != "" {
|
|
vkStatus = "yes"
|
|
}
|
|
|
|
cmdLine := spec.Harness.Binary
|
|
if spec.Harness.RunArgsForMod != nil {
|
|
if a := spec.Harness.RunArgsForMod(spec.Model); len(a) > 0 {
|
|
cmdLine += " " + strings.Join(a, " ")
|
|
}
|
|
}
|
|
if spec.Worktree != "" && spec.Harness.WorktreeArgs != nil {
|
|
if a := spec.Harness.WorktreeArgs(spec.Worktree); len(a) > 0 {
|
|
cmdLine += " " + strings.Join(a, " ")
|
|
}
|
|
}
|
|
|
|
cyan := "\033[36m"
|
|
dim := "\033[2m"
|
|
bold := "\033[1m"
|
|
reset := "\033[0m"
|
|
|
|
var b strings.Builder
|
|
b.WriteString("\n")
|
|
b.WriteString(dim + "───────────────────────────────────────────────────" + reset + "\n")
|
|
b.WriteString(cyan + "bifrost>" + reset + " " + bold + spec.Harness.Label + reset + " " + dim + spec.Model + reset + "\n")
|
|
b.WriteString(dim + " endpoint : " + reset + endpoint + "\n")
|
|
b.WriteString(dim + " vk : " + reset + vkStatus + "\n")
|
|
b.WriteString(dim + " command : " + reset + cmdLine + "\n")
|
|
b.WriteString(dim + "───────────────────────────────────────────────────" + reset + "\n")
|
|
b.WriteString("\n")
|
|
return b.String()
|
|
}
|