Files
2026-07-13 13:33:09 +08:00

235 lines
5.0 KiB
Go

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
}