You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

192 lines
4.4 KiB

4 weeks ago
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
}