Skip to content

Commit fd3c7db

Browse files
authored
Merge pull request #2 from gcclinux/mrwhiz-python-support
added workspace option
2 parents d56cc31 + 75b3b9d commit fd3c7db

6 files changed

Lines changed: 710 additions & 27 deletions

File tree

internal/executor/executor.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package executor
33
import (
44
"bytes"
55
"fmt"
6+
"os"
67
"os/exec"
78
"path/filepath"
89
"runtime"
@@ -154,7 +155,19 @@ func (ce *CommandExecutor) GetInterpreter(scriptPath string) string {
154155
// On Unix systems, use pwsh (PowerShell Core) if available
155156
return "pwsh"
156157
case ".py":
157-
// Python script
158+
// Python script - prefer venv Python if available
159+
scriptDir := filepath.Dir(scriptPath)
160+
if runtime.GOOS == "windows" {
161+
venvPython := filepath.Join(scriptDir, "venv", "Scripts", "python.exe")
162+
if _, err := os.Stat(venvPython); err == nil {
163+
return venvPython
164+
}
165+
return "python"
166+
}
167+
venvPython := filepath.Join(scriptDir, "venv", "bin", "python")
168+
if _, err := os.Stat(venvPython); err == nil {
169+
return venvPython
170+
}
158171
return "python3"
159172
case ".go":
160173
// Go source file - use "go run" for execution

internal/filemanager/filemanager.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,63 @@ func (fm *FileManager) ListFiles() ([]string, error) {
264264
return files, nil
265265
}
266266

267+
// ListDirectories returns a list of subdirectories in the given directory
268+
func (fm *FileManager) ListDirectories(dirPath string) ([]string, error) {
269+
entries, err := os.ReadDir(dirPath)
270+
if err != nil {
271+
return nil, err
272+
}
273+
274+
var dirs []string
275+
for _, entry := range entries {
276+
if entry.IsDir() && entry.Name()[0] != '.' {
277+
dirs = append(dirs, entry.Name())
278+
}
279+
}
280+
return dirs, nil
281+
}
282+
283+
// ListEntries returns directories and files in the given directory (non-recursive)
284+
func (fm *FileManager) ListEntries(dirPath string) ([]string, []string, error) {
285+
entries, err := os.ReadDir(dirPath)
286+
if err != nil {
287+
return nil, nil, err
288+
}
289+
290+
var dirs []string
291+
var files []string
292+
293+
// Image extensions to exclude (same as ListFiles)
294+
imageExts := map[string]bool{
295+
".ico": true,
296+
".png": true,
297+
".jpg": true,
298+
".jpeg": true,
299+
".bmp": true,
300+
".gif": true,
301+
".svg": true,
302+
".webp": true,
303+
}
304+
305+
for _, entry := range entries {
306+
name := entry.Name()
307+
if name[0] == '.' {
308+
continue // Skip hidden files/folders
309+
}
310+
311+
if entry.IsDir() {
312+
dirs = append(dirs, name)
313+
} else {
314+
ext := strings.ToLower(filepath.Ext(name))
315+
if !imageExts[ext] {
316+
files = append(files, name)
317+
}
318+
}
319+
}
320+
321+
return dirs, files, nil
322+
}
323+
267324
// resolvePath resolves a relative path to an absolute path within the workspace
268325
func (fm *FileManager) resolvePath(path string) string {
269326
// Clean the path to prevent directory traversal

internal/installer/installer.go

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,3 +472,208 @@ func (li *LanguageInstaller) CheckLanguageForFile(fileType string) (bool, string
472472
return true, ""
473473
}
474474
}
475+
476+
// GetPythonInstallCommand returns the appropriate command to install Python based on OS
477+
func (li *LanguageInstaller) GetPythonInstallCommand() (string, string, error) {
478+
switch runtime.GOOS {
479+
case "windows":
480+
return "winget", "winget install -e --id Python.Python.3.12", nil
481+
case "darwin":
482+
return "brew", "brew install python@3.12", nil
483+
case "linux":
484+
// Try to detect package manager
485+
if _, err := exec.LookPath("apt"); err == nil {
486+
return "apt", "sudo apt update && sudo apt install -y python3 python3-venv python3-pip", nil
487+
}
488+
if _, err := exec.LookPath("dnf"); err == nil {
489+
return "dnf", "sudo dnf install -y python3 python3-pip", nil
490+
}
491+
if _, err := exec.LookPath("pacman"); err == nil {
492+
return "pacman", "sudo pacman -S --noconfirm python python-pip", nil
493+
}
494+
return "manual", "Download and install from https://www.python.org/downloads/", nil
495+
default:
496+
return "", "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
497+
}
498+
}
499+
500+
// InstallPython attempts to install Python using the appropriate method for the OS
501+
func (li *LanguageInstaller) InstallPython() (string, error) {
502+
switch runtime.GOOS {
503+
case "linux":
504+
return li.InstallPythonLinux()
505+
case "windows":
506+
return li.InstallPythonWindows()
507+
case "darwin":
508+
return li.InstallPythonDarwin()
509+
default:
510+
return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
511+
}
512+
}
513+
514+
// InstallPythonWindows installs Python on Windows using winget
515+
func (li *LanguageInstaller) InstallPythonWindows() (string, error) {
516+
var output strings.Builder
517+
output.WriteString("Starting Python installation for Windows...\n\n")
518+
li.reportProgress("🚀 Starting Python installation for Windows...")
519+
520+
// Check if winget is available
521+
output.WriteString("1. Checking for winget...\n")
522+
li.reportProgress("🔍 Checking for winget...")
523+
checkCmd := exec.Command("winget", "--version")
524+
if err := checkCmd.Run(); err != nil {
525+
return output.String(), fmt.Errorf("winget is not installed or not in PATH")
526+
}
527+
output.WriteString(" winget is available\n\n")
528+
li.reportProgress("✓ winget is available")
529+
530+
// Install Python
531+
output.WriteString("2. Installing Python via winget...\n")
532+
li.reportProgress("⬇️ Installing Python via winget... This may take a few minutes...")
533+
cmd := exec.Command("powershell", "-NoProfile", "-Command", "winget install -e --id Python.Python.3.12")
534+
cmdOutput, err := cmd.CombinedOutput()
535+
output.WriteString(string(cmdOutput))
536+
537+
if err != nil {
538+
return output.String(), fmt.Errorf("installation failed: %w", err)
539+
}
540+
541+
output.WriteString("\n✓ Python installation complete!\n")
542+
output.WriteString("\nIMPORTANT: Please restart Terminal Intelligence to update your PATH.\n")
543+
li.reportProgress("🎉 Python installation complete!")
544+
li.reportProgress("⚠️ IMPORTANT: Restart Terminal Intelligence to update PATH")
545+
546+
return output.String(), nil
547+
}
548+
549+
// InstallPythonLinux installs Python on Linux using the system package manager
550+
func (li *LanguageInstaller) InstallPythonLinux() (string, error) {
551+
var output strings.Builder
552+
output.WriteString("Starting Python installation for Linux...\n\n")
553+
li.reportProgress("🚀 Starting Python installation for Linux...")
554+
555+
// Detect package manager
556+
output.WriteString("1. Detecting package manager...\n")
557+
li.reportProgress("🔍 Detecting package manager...")
558+
559+
var installCmd *exec.Cmd
560+
var pkgManager string
561+
562+
if _, err := exec.LookPath("apt"); err == nil {
563+
pkgManager = "apt"
564+
output.WriteString(" Detected: apt (Debian/Ubuntu)\n\n")
565+
li.reportProgress("✓ Detected: apt (Debian/Ubuntu)")
566+
567+
// Update package list
568+
output.WriteString("2. Updating package list...\n")
569+
li.reportProgress("📡 Updating package list...")
570+
updateCmd := exec.Command("sudo", "apt", "update")
571+
if updateOutput, err := updateCmd.CombinedOutput(); err != nil {
572+
output.WriteString(fmt.Sprintf(" Warning: apt update failed: %v\n", err))
573+
output.WriteString(string(updateOutput))
574+
} else {
575+
output.WriteString(" Package list updated\n")
576+
li.reportProgress("✓ Package list updated")
577+
}
578+
output.WriteString("\n")
579+
580+
output.WriteString("3. Installing Python...\n")
581+
li.reportProgress("⬇️ Installing Python packages...")
582+
installCmd = exec.Command("sudo", "apt", "install", "-y", "python3", "python3-venv", "python3-pip")
583+
} else if _, err := exec.LookPath("dnf"); err == nil {
584+
pkgManager = "dnf"
585+
output.WriteString(" Detected: dnf (Fedora/RHEL)\n\n")
586+
li.reportProgress("✓ Detected: dnf (Fedora/RHEL)")
587+
588+
output.WriteString("2. Installing Python...\n")
589+
li.reportProgress("⬇️ Installing Python packages...")
590+
installCmd = exec.Command("sudo", "dnf", "install", "-y", "python3", "python3-pip")
591+
} else if _, err := exec.LookPath("pacman"); err == nil {
592+
pkgManager = "pacman"
593+
output.WriteString(" Detected: pacman (Arch Linux)\n\n")
594+
li.reportProgress("✓ Detected: pacman (Arch Linux)")
595+
596+
output.WriteString("2. Installing Python...\n")
597+
li.reportProgress("⬇️ Installing Python packages...")
598+
installCmd = exec.Command("sudo", "pacman", "-S", "--noconfirm", "python", "python-pip")
599+
} else {
600+
return output.String(), fmt.Errorf("no supported package manager found (apt, dnf, or pacman). Please install Python manually from https://www.python.org/downloads/")
601+
}
602+
603+
_ = pkgManager // used for logging above
604+
605+
cmdOutput, err := installCmd.CombinedOutput()
606+
output.WriteString(string(cmdOutput))
607+
608+
if err != nil {
609+
return output.String(), fmt.Errorf("installation failed: %w", err)
610+
}
611+
612+
output.WriteString("\n")
613+
614+
// Verify installation
615+
output.WriteString("4. Verifying installation...\n")
616+
li.reportProgress("🔍 Verifying installation...")
617+
618+
verifyCmd := exec.Command("python3", "--version")
619+
if verifyOutput, err := verifyCmd.Output(); err != nil {
620+
output.WriteString(fmt.Sprintf(" Warning: Could not verify: %v\n", err))
621+
li.reportProgress("⚠️ Warning: Could not verify installation")
622+
} else {
623+
versionOutput := strings.TrimSpace(string(verifyOutput))
624+
output.WriteString(fmt.Sprintf(" %s\n", versionOutput))
625+
li.reportProgress(fmt.Sprintf("✓ %s", versionOutput))
626+
}
627+
output.WriteString("\n")
628+
629+
output.WriteString("✓ Python installation complete!\n")
630+
li.reportProgress("🎉 Python installation complete!")
631+
632+
return output.String(), nil
633+
}
634+
635+
// InstallPythonDarwin installs Python on macOS using Homebrew
636+
func (li *LanguageInstaller) InstallPythonDarwin() (string, error) {
637+
var output strings.Builder
638+
output.WriteString("Starting Python installation for macOS...\n\n")
639+
li.reportProgress("🚀 Starting Python installation for macOS...")
640+
641+
// Check if brew is available
642+
output.WriteString("1. Checking for Homebrew...\n")
643+
li.reportProgress("🔍 Checking for Homebrew...")
644+
checkCmd := exec.Command("brew", "--version")
645+
if err := checkCmd.Run(); err != nil {
646+
return output.String(), fmt.Errorf("Homebrew is not installed. Install from: https://brew.sh")
647+
}
648+
output.WriteString(" Homebrew is available\n\n")
649+
li.reportProgress("✓ Homebrew is available")
650+
651+
// Update brew
652+
output.WriteString("2. Updating Homebrew...\n")
653+
li.reportProgress("📡 Updating Homebrew...")
654+
updateCmd := exec.Command("brew", "update")
655+
if _, err := updateCmd.CombinedOutput(); err != nil {
656+
output.WriteString(fmt.Sprintf(" Warning: brew update failed: %v\n", err))
657+
} else {
658+
output.WriteString(" Homebrew updated\n")
659+
li.reportProgress("✓ Homebrew updated")
660+
}
661+
output.WriteString("\n")
662+
663+
// Install Python
664+
output.WriteString("3. Installing Python via Homebrew...\n")
665+
li.reportProgress("⬇️ Installing Python via Homebrew... This may take a few minutes...")
666+
cmd := exec.Command("brew", "install", "python@3.12")
667+
cmdOutput, err := cmd.CombinedOutput()
668+
output.WriteString(string(cmdOutput))
669+
670+
if err != nil {
671+
return output.String(), fmt.Errorf("installation failed: %w", err)
672+
}
673+
674+
output.WriteString("\n✓ Python installation complete!\n")
675+
output.WriteString("\nPython should now be available in your PATH.\n")
676+
li.reportProgress("🎉 Python installation complete!")
677+
678+
return output.String(), nil
679+
}

internal/ui/aichat.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,79 @@ func (a *AIChatPane) executeCommand(script string, cwd string) tea.Cmd {
683683
}
684684
}
685685

686+
// Preliminary Python venv initialization check
687+
if strings.Contains(script, "pip install") || strings.Contains(script, "pip3 install") || strings.Contains(script, "python ") || strings.Contains(script, "python3 ") {
688+
if effectiveDir != "" {
689+
venvDir := filepath.Join(effectiveDir, "venv")
690+
if _, err := os.Stat(venvDir); os.IsNotExist(err) {
691+
// Determine python command
692+
pythonCmd := ""
693+
if runtime.GOOS == "windows" {
694+
if _, err := exec.LookPath("python"); err == nil {
695+
pythonCmd = "python"
696+
} else if _, err := exec.LookPath("python3"); err == nil {
697+
pythonCmd = "python3"
698+
}
699+
} else {
700+
if _, err := exec.LookPath("python3"); err == nil {
701+
pythonCmd = "python3"
702+
} else if _, err := exec.LookPath("python"); err == nil {
703+
pythonCmd = "python"
704+
}
705+
}
706+
707+
if pythonCmd != "" {
708+
createCmd := exec.Command(pythonCmd, "-m", "venv", "venv")
709+
createCmd.Dir = effectiveDir
710+
createCmd.Run()
711+
}
712+
}
713+
714+
// Install requirements.txt if it exists
715+
reqFile := filepath.Join(effectiveDir, "requirements.txt")
716+
if _, err := os.Stat(reqFile); err == nil {
717+
var pipCmd string
718+
if runtime.GOOS == "windows" {
719+
venvPip := filepath.Join(effectiveDir, "venv", "Scripts", "pip.exe")
720+
if _, err := os.Stat(venvPip); err == nil {
721+
pipCmd = venvPip
722+
} else {
723+
pipCmd = "pip"
724+
}
725+
} else {
726+
venvPip := filepath.Join(effectiveDir, "venv", "bin", "pip")
727+
if _, err := os.Stat(venvPip); err == nil {
728+
pipCmd = venvPip
729+
} else {
730+
pipCmd = "pip3"
731+
}
732+
}
733+
installCmd := exec.Command(pipCmd, "install", "-r", reqFile)
734+
installCmd.Dir = effectiveDir
735+
installCmd.Run()
736+
}
737+
738+
// Rewrite the script to use venv python/pip if available
739+
var venvPython, venvPip string
740+
if runtime.GOOS == "windows" {
741+
venvPython = filepath.Join(effectiveDir, "venv", "Scripts", "python.exe")
742+
venvPip = filepath.Join(effectiveDir, "venv", "Scripts", "pip.exe")
743+
} else {
744+
venvPython = filepath.Join(effectiveDir, "venv", "bin", "python")
745+
venvPip = filepath.Join(effectiveDir, "venv", "bin", "pip")
746+
}
747+
748+
if _, err := os.Stat(venvPython); err == nil {
749+
script = strings.ReplaceAll(script, "python3 ", venvPython+" ")
750+
script = strings.ReplaceAll(script, "python ", venvPython+" ")
751+
}
752+
if _, err := os.Stat(venvPip); err == nil {
753+
script = strings.ReplaceAll(script, "pip3 install", venvPip+" install")
754+
script = strings.ReplaceAll(script, "pip install", venvPip+" install")
755+
}
756+
}
757+
}
758+
686759
var cmd *exec.Cmd
687760
if runtime.GOOS == "windows" {
688761
// For Windows, convert newlines to semicolons for PowerShell

0 commit comments

Comments
 (0)