Go · 5298 bytes Raw Blame History
1 package cmd
2
3 import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "strings"
8
9 "github.com/spf13/cobra"
10 )
11
12 var installCmd = &cobra.Command{
13 Use: "install",
14 Short: "Install parrot shell hooks",
15 Long: "Adds parrot hooks to your shell configuration",
16 Run: installHooks,
17 }
18
19 func init() {
20 rootCmd.AddCommand(installCmd)
21 }
22
23 func installHooks(cmd *cobra.Command, args []string) {
24 homeDir, err := os.UserHomeDir()
25 if err != nil {
26 fmt.Printf("❌ Error getting home directory: %v\n", err)
27 return
28 }
29
30 // Detect shell and appropriate RC file
31 shell := os.Getenv("SHELL")
32 var rcFile string
33 shellName := filepath.Base(shell)
34
35 if shellName == "fish" {
36 // Fish uses a different config directory structure
37 rcFile = filepath.Join(homeDir, ".config/fish/conf.d/parrot.fish")
38 // Create directory if needed
39 confDir := filepath.Join(homeDir, ".config/fish/conf.d")
40 if err := os.MkdirAll(confDir, 0755); err != nil {
41 fmt.Printf("❌ Error creating fish config directory: %v\n", err)
42 return
43 }
44 } else if shellName == "zsh" {
45 rcFile = filepath.Join(homeDir, ".zshrc")
46 } else {
47 rcFile = filepath.Join(homeDir, ".bashrc")
48 }
49
50 // Try standard installation paths for the hook script
51 var hookPath string
52 var hookFile string
53 if shellName == "fish" {
54 hookFile = "parrot-hook.fish"
55 } else {
56 hookFile = "parrot-hook.sh"
57 }
58
59 // Build list of possible paths, starting with most specific
60 possiblePaths := []string{}
61
62 // Check relative to executable (works for NixOS, Homebrew, and other non-FHS systems)
63 if exePath, err := os.Executable(); err == nil {
64 // Resolve symlinks to get actual binary location
65 if realPath, err := filepath.EvalSymlinks(exePath); err == nil {
66 exePath = realPath
67 }
68 exeDir := filepath.Dir(exePath)
69 // Try ../share/parrot/ relative to bin directory
70 possiblePaths = append(possiblePaths, filepath.Join(exeDir, "..", "share", "parrot", hookFile))
71 }
72
73 // Standard FHS and user paths
74 possiblePaths = append(possiblePaths,
75 filepath.Join("/usr/share/parrot", hookFile), // RPM/system installation
76 filepath.Join("/usr/local/share/parrot", hookFile), // Manual system installation
77 filepath.Join(homeDir, ".local/share/parrot", hookFile), // make install (user local)
78 filepath.Join(".", hookFile), // Development
79 )
80
81 for _, path := range possiblePaths {
82 if _, err := os.Stat(path); err == nil {
83 hookPath = path
84 break
85 }
86 }
87
88 if hookPath == "" {
89 fmt.Println("❌ Hook script not found. Searched in:")
90 for _, path := range possiblePaths {
91 fmt.Printf(" - %s\n", path)
92 }
93 fmt.Println("Make sure parrot is properly installed.")
94 return
95 }
96
97 fmt.Printf("🦜 Installing parrot hooks to: %s\n", rcFile)
98 fmt.Printf("🔧 Configuring Ollama for better performance\n")
99
100 if shellName == "fish" {
101 // For fish, copy the hook file directly to conf.d
102 // Fish automatically sources files in conf.d
103 hookContent, err := os.ReadFile(hookPath)
104 if err != nil {
105 fmt.Printf("❌ Error reading hook file: %v\n", err)
106 return
107 }
108
109 // Check if already installed
110 if _, err := os.Stat(rcFile); err == nil {
111 fmt.Println("✅ Parrot hooks already installed!")
112 return
113 }
114
115 // Write hook file with Ollama configuration
116 file, err := os.OpenFile(rcFile, os.O_CREATE|os.O_WRONLY, 0644)
117 if err != nil {
118 fmt.Printf("❌ Error creating %s: %v\n", rcFile, err)
119 return
120 }
121 defer file.Close()
122
123 fishConfig := fmt.Sprintf(`# Parrot CLI hooks and configuration
124 # Keep AI models loaded for better performance
125 set -gx OLLAMA_KEEP_ALIVE "1h"
126
127 %s`, string(hookContent))
128
129 _, err = file.WriteString(fishConfig)
130 if err != nil {
131 fmt.Printf("❌ Error writing to %s: %v\n", rcFile, err)
132 return
133 }
134
135 fmt.Println("✅ Parrot hooks installed successfully!")
136 fmt.Println("🔄 Restart fish or run 'source ~/.config/fish/config.fish' to activate.")
137 } else {
138 // For bash/zsh, add source line to RC file
139 sourceLine := fmt.Sprintf("source \"%s\"", hookPath)
140 fmt.Printf("📝 Adding hook: %s\n", sourceLine)
141
142 // Check if already installed
143 if isAlreadyInstalled(rcFile, sourceLine) {
144 fmt.Println("✅ Parrot hooks already installed!")
145 return
146 }
147
148 // Append to RC file
149 file, err := os.OpenFile(rcFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
150 if err != nil {
151 fmt.Printf("❌ Error opening %s: %v\n", rcFile, err)
152 return
153 }
154 defer file.Close()
155
156 installContent := fmt.Sprintf(`
157 # Parrot CLI hooks and configuration
158 export OLLAMA_KEEP_ALIVE="1h" # Keep AI models loaded for better performance
159 %s
160 `, sourceLine)
161
162 _, err = file.WriteString(installContent)
163 if err != nil {
164 fmt.Printf("❌ Error writing to %s: %v\n", rcFile, err)
165 return
166 }
167
168 fmt.Println("✅ Parrot hooks installed successfully!")
169 if shellName == "zsh" {
170 fmt.Println("🔄 Run 'source ~/.zshrc' to activate, or start a new shell session.")
171 } else {
172 fmt.Println("🔄 Run 'source ~/.bashrc' to activate, or start a new shell session.")
173 }
174 }
175 }
176
177 func isAlreadyInstalled(rcFile, sourceLine string) bool {
178 content, err := os.ReadFile(rcFile)
179 if err != nil {
180 return false
181 }
182
183 // Check for both the source line and OLLAMA_KEEP_ALIVE setting
184 contentStr := string(content)
185 return strings.Contains(contentStr, sourceLine) && strings.Contains(contentStr, "OLLAMA_KEEP_ALIVE")
186 }