package cli import ( "bytes" "fmt" "os" "os/exec" "path/filepath" "strings" ) const ( pathMarker = "__TERACLONE_PWD__=" ) type TerminalSession struct { workingDir string } type CommandResult struct { Prompt string `json:"prompt"` WorkingDir string `json:"workingDir"` Output string `json:"output"` } func NewTerminalSession() (*TerminalSession, error) { workingDir, err := os.Getwd() if err != nil { return nil, err } return &TerminalSession{ workingDir: workingDir, }, nil } func (s *TerminalSession) Prompt() string { return fmt.Sprintf("PS %s>", s.workingDir) } func (s *TerminalSession) WelcomeMessage() string { return "Connected to local PowerShell bridge.\r\nType a Windows command and press Enter.\r\nUse 'clear' or 'cls' to reset the screen.\r\n" } func (s *TerminalSession) Snapshot() CommandResult { return CommandResult{ Prompt: s.Prompt(), WorkingDir: s.workingDir, } } func (s *TerminalSession) Execute(command string) CommandResult { command = strings.TrimSpace(command) if command == "" { return s.Snapshot() } if handled, result := s.handleChangeDirectory(command); handled { return result } output, workingDir, err := runPowerShellCommand(s.workingDir, command) if workingDir != "" { s.workingDir = workingDir } result := s.Snapshot() result.Output = normalizeOutput(output) if err != nil { if result.Output != "" && !strings.HasSuffix(result.Output, "\r\n") { result.Output += "\r\n" } result.Output += fmt.Sprintf("error: %v\r\n", err) } return result } func (s *TerminalSession) handleChangeDirectory(command string) (bool, CommandResult) { trimmed := strings.TrimSpace(command) lower := strings.ToLower(trimmed) if lower != "cd" && !strings.HasPrefix(lower, "cd ") && !strings.HasPrefix(lower, "chdir ") { return false, CommandResult{} } target := "." switch { case lower == "cd": target = os.Getenv("USERPROFILE") if target == "" { target = s.workingDir } case strings.HasPrefix(lower, "chdir "): target = strings.TrimSpace(trimmed[6:]) default: target = strings.TrimSpace(trimmed[2:]) } target = strings.Trim(target, "\"") target = strings.TrimSpace(target) if target == "" { target = s.workingDir } if !filepath.IsAbs(target) { target = filepath.Join(s.workingDir, target) } resolved, err := filepath.Abs(target) if err != nil { result := s.Snapshot() result.Output = fmt.Sprintf("error: %v\r\n", err) return true, result } info, err := os.Stat(resolved) if err != nil { result := s.Snapshot() result.Output = fmt.Sprintf("error: %v\r\n", err) return true, result } if !info.IsDir() { result := s.Snapshot() result.Output = "error: target is not a directory\r\n" return true, result } s.workingDir = resolved return true, s.Snapshot() } func runPowerShellCommand(workingDir string, command string) (string, string, error) { var stdout bytes.Buffer var stderr bytes.Buffer script := buildPowerShellScript(workingDir, command) cmd := exec.Command("powershell", "-NoLogo", "-NoProfile", "-Command", script) cmd.Stdout = &stdout cmd.Stderr = &stderr err := cmd.Run() combined := stdout.String() if stderr.Len() > 0 { combined += stderr.String() } output, nextDir := splitOutputAndDirectory(combined) return output, nextDir, err } func buildPowerShellScript(workingDir string, command string) string { escapedDir := strings.ReplaceAll(workingDir, "'", "''") return strings.Join([]string{ "$OutputEncoding = [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()", "$ErrorActionPreference = 'Continue'", fmt.Sprintf("Set-Location -LiteralPath '%s'", escapedDir), command, fmt.Sprintf("Write-Output ('%s' + (Get-Location).Path)", pathMarker), }, "; ") } func splitOutputAndDirectory(raw string) (string, string) { normalized := strings.ReplaceAll(raw, "\r\n", "\n") lines := strings.Split(normalized, "\n") workingDir := "" filtered := make([]string, 0, len(lines)) for _, line := range lines { if strings.HasPrefix(line, pathMarker) { workingDir = strings.TrimPrefix(line, pathMarker) continue } filtered = append(filtered, line) } output := strings.Join(filtered, "\n") output = strings.TrimRight(output, "\n") if output != "" { output += "\r\n" } return output, workingDir } func normalizeOutput(output string) string { output = strings.ReplaceAll(output, "\r\n", "\n") output = strings.ReplaceAll(output, "\n", "\r\n") return output }