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
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
|
||
|
|
}
|