package lib import ( "encoding/json" "os" "os/exec" "os/signal" "path/filepath" "plandex-cli/fs" "plandex-cli/term" "strconv" "syscall" ) var ReplSettingsDir string type ReplMode string const ( ReplModeTell ReplMode = "tell" ReplModeChat ReplMode = "chat" ) type ReplState struct { Mode ReplMode IsMulti bool } var CurrentReplState = ReplState{ Mode: ReplModeChat, IsMulti: false, } type ReplSettings struct { State ReplState History []string } var ReplCmdAliases = map[string]string{ "chat": "ch", "tell": "t", "multi": "m", "quit": "q", "help": "h", "run": "r", "send": "s", } func init() { ReplSettingsDir = filepath.Join(fs.HomePlandexDir, "repl_settings") } func EnsureReplSettingsFile() { if err := os.MkdirAll(ReplSettingsDir, os.ModePerm); err != nil { term.OutputErrorAndExit("Error creating repl history directory: %v", err) } settingsFile := filepath.Join(ReplSettingsDir, CurrentProjectId+".json") if _, err := os.Stat(settingsFile); os.IsNotExist(err) { file, err := os.Create(settingsFile) if err != nil { term.OutputErrorAndExit("Error creating history file: %v", err) } defer file.Close() // Write empty settings object var settings ReplSettings data, err := json.Marshal(settings) if err != nil { term.OutputErrorAndExit("Error converting settings to JSON: %v", err) } if _, err := file.Write(data); err != nil { term.OutputErrorAndExit("Error writing to history file: %v", err) } } } func writeSettings(settings *ReplSettings) { settingsFile := filepath.Join(ReplSettingsDir, CurrentProjectId+".json") data, err := json.Marshal(settings) if err != nil { term.OutputErrorAndExit("Error converting settings to JSON: %v", err) } if err := os.WriteFile(settingsFile, data, 0644); err != nil { term.OutputErrorAndExit("Error writing settings file: %v", err) } } func getSettings() *ReplSettings { EnsureReplSettingsFile() settingsFile := filepath.Join(ReplSettingsDir, CurrentProjectId+".json") // Read existing settings data, err := os.ReadFile(settingsFile) if err != nil { term.OutputErrorAndExit("Error reading history file: %v", err) } // Parse JSON var settings ReplSettings if err := json.Unmarshal(data, &settings); err != nil { term.OutputErrorAndExit("Error parsing history file: %v", err) } return &settings } func LoadState() { settings := getSettings() if settings.State.Mode != "" { CurrentReplState = settings.State } else { // Write default state WriteState() } } func WriteState() { settings := getSettings() settings.State = CurrentReplState writeSettings(settings) } func WriteHistory(input string) { settings := getSettings() // Add new input settings.History = append(settings.History, input) writeSettings(settings) } func GetHistory() []string { settings := getSettings() return settings.History } // ExecPlandexCommand spawns the same binary, wiring std streams directly so you // don't have to capture output. Any os.Exit calls in the child won't kill your REPL. func ExecPlandexCommand(args []string) (string, error) { return ExecPlandexCommandWithParams(args, ExecPlandexCommandParams{}) } type ExecPlandexCommandParams struct { DisableSuggestions bool SessionId string } func ExecPlandexCommandWithParams(args []string, params ExecPlandexCommandParams) (string, error) { // Create temp file tmpFile, err := os.CreateTemp("", "plandex-output-*") if err != nil { return "", err } tmpPath := tmpFile.Name() tmpFile.Close() defer os.Remove(tmpPath) var env []string = os.Environ() if os.Getenv("PLANDEX_REPL") == "" { columns := term.GetTerminalWidth() hasDarkBackground := term.HasDarkBackground() streamForegroundColor := term.GetStreamForegroundColor() var glamourStyle string if hasDarkBackground { glamourStyle = "dark" } else { glamourStyle = "light" } // Set env vars env = append(env, "PLANDEX_REPL=1", "PLANDEX_REPL_OUTPUT_FILE="+tmpPath, "PLANDEX_COLUMNS="+strconv.Itoa(columns), "PLANDEX_STREAM_FOREGROUND_COLOR="+streamForegroundColor.Sequence(false), "GLAMOUR_STYLE="+glamourStyle, "PLANDEX_SKIP_UPGRADE=1", ) if params.SessionId != "" { env = append(env, "PLANDEX_REPL_SESSION_ID="+params.SessionId) } } if params.DisableSuggestions { env = append(env, "PLANDEX_DISABLE_SUGGESTIONS=1") } // Run command cmd := exec.Command(os.Args[0], args...) cmd.Env = env // Connect stdin directly cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err = cmd.Start() if err != nil { return "", err } signal.Ignore(syscall.SIGINT) defer signal.Reset(syscall.SIGINT) sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) go func() { sig := <-sigChan syscall.Kill(-cmd.Process.Pid, sig.(syscall.Signal)) }() err = cmd.Wait() if err != nil { if _, ok := err.(*exec.ExitError); ok { return "", nil } return "", err } // Read output from temp file output, err := os.ReadFile(tmpPath) if err != nil { return "", err } return string(output), nil }