11package main
22
33import (
4+ "bufio"
45 "bytes"
56 "docker-ai/pkg/config"
67 "flag"
@@ -73,11 +74,20 @@ func runAIMode() {
7374 fmt .Printf ("Warning: could not load config file: %v\n " , err )
7475 }
7576
76- modelPluginAvailable := true
7777 llmProvider := flag .String ("llm-provider" , "groq" , "LLM provider to use (groq, gemini, openai)" )
78- model := flag .String ("model" , "gemma2-9b-it" , "Model to use" )
78+ model := flag .String ("model" , "gemma-3n-e4b-it" , "Model to use" )
79+ command := flag .String ("c" , "" , "Execute a single command and exit" )
7980 flag .Parse ()
8081
82+ if * command != "" {
83+ runSingleCommand (& appConfig , * command , * llmProvider , * model )
84+ return
85+ }
86+
87+ runInteractiveMode (& appConfig , * llmProvider , * model )
88+ }
89+
90+ func runInteractiveMode (appConfig * config.Config , llmProvider , model string ) {
8191 fmt .Println ("Docker AI interactive shell. Type 'exit' or 'quit' to leave." )
8292
8393 historyFile := filepath .Join (os .Getenv ("HOME" ), ".docker-ai-history" )
@@ -91,7 +101,6 @@ func runAIMode() {
91101 f .Close ()
92102 }
93103
94- lastContainerName := ""
95104 for {
96105 input , err := line .Prompt ("docker-ai> " )
97106 if err != nil {
@@ -112,87 +121,10 @@ func runAIMode() {
112121 break
113122 }
114123
115- if input == "reset confirm" {
116- appConfig .SkipCleanupWarning = false
117- if err := config .SaveConfig (appConfig ); err != nil {
118- fmt .Println ("Failed to save configuration:" , err )
119- } else {
120- fmt .Println ("Cleanup confirmation has been reset. You will be prompted before cleanup commands are run." )
121- }
122- continue
123- }
124-
125- // Replace 'that container' with lastContainerName if present
126- userInput := input
127- if strings .Contains (input , "that container" ) && lastContainerName != "" {
128- userInput = strings .ReplaceAll (input , "that container" , lastContainerName )
129- }
130-
131- // Get running containers to provide context to the LLM
132- var fullPrompt string
133- psCmd := exec .Command ("sh" , "-c" , "docker ps -a --format '{{.Names}} ({{.Status}})'" )
134- psOutput , err := psCmd .CombinedOutput ()
135- if err == nil && len (psOutput ) > 0 {
136- containerList := strings .TrimSpace (string (psOutput ))
137- containerList = strings .ReplaceAll (containerList , "\n " , ", " )
138- fullPrompt = fmt .Sprintf ("The user wants to perform a Docker command. Here is a list of all containers (running and stopped): %s.\n \n User's request: %s" , containerList , userInput )
139- } else {
140- fullPrompt = userInput
141- }
142-
143- if ! modelPluginAvailable && strings .Contains (userInput , "model" ) {
144- fullPrompt += "\n \n Note: The 'docker model' command is not available on this system."
145- }
146-
147- response , err := llm .QueryLLM (fullPrompt , * llmProvider , * model )
148- if err != nil {
149- fmt .Printf ("Error: %v\n " , err )
150- continue
151- }
152-
153- if ! strings .HasPrefix (response , "docker " ) {
154- fmt .Println (response )
155- continue
156- }
157-
158- // Cleanup command confirmation
159- if isCleanupCommand (response ) && ! appConfig .SkipCleanupWarning {
160- fmt .Printf ("WARNING: The generated command is a cleanup command:\n %s\n \n " , response )
161-
162- answer , err := line .Prompt ("Are you sure you want to execute? [y]es, [n]o, [d]on't ask again: " )
163- if err != nil {
164- fmt .Println ("Execution cancelled." )
165- continue
166- }
167- answer = strings .ToLower (strings .TrimSpace (answer ))
168-
169- switch answer {
170- case "y" , "yes" :
171- // continue to execution
172- case "d" , "dont" , "don't ask again" :
173- appConfig .SkipCleanupWarning = true
174- if err := config .SaveConfig (appConfig ); err != nil {
175- fmt .Println ("Failed to save configuration:" , err )
176- }
177- // continue to execution
178- default :
179- fmt .Println ("Execution cancelled." )
180- continue
181- }
182- }
183-
184- fmt .Printf ("➜ executing: %s\n " , response )
185-
186124 // Before running the command, close the liner to restore the terminal
187125 line .Close ()
188126
189- // Execute the Docker command
190- var stderrBuf bytes.Buffer
191- cmd := exec .Command ("sh" , "-c" , response )
192- cmd .Stdin = os .Stdin // Pass the terminal's input to the command
193- cmd .Stdout = os .Stdout
194- cmd .Stderr = io .MultiWriter (os .Stderr , & stderrBuf )
195- err = cmd .Run ()
127+ runSingleCommand (appConfig , input , llmProvider , model )
196128
197129 // After a command that takes over stdin, the terminal can be left in a
198130 // "raw" state. We use `stty` to force it back to a sane mode before
@@ -208,67 +140,152 @@ func runAIMode() {
208140 line .ReadHistory (f )
209141 f .Close ()
210142 }
143+ }
211144
212- if err != nil {
213- if exitErr , ok := err .(* exec.ExitError ); ok {
214- // The command exited with a non-zero status.
215- // Stderr is already printed. We can analyze it from the buffer.
216- stderrString := stderrBuf .String ()
217- if strings .Contains (response , "docker model" ) {
218- if strings .Contains (stderrString , "is not a docker command" ) {
219- modelPluginAvailable = false
220- fmt .Println ("\n Error: The 'docker model' command is not available." )
221- fmt .Println ("Please ensure you have installed the Docker Model Runner plugin." )
222- fmt .Println ("You can try installing it with: 'sudo apt-get update && sudo apt-get install docker-model-plugin'" )
223- }
224- }
225- // Check for docker scout update message
226- if strings .Contains (stderrString , "New version" ) && strings .Contains (stderrString , "available" ) {
227- fmt .Println ("\n Hint: A new version of Docker Scout is available." )
228- fmt .Println ("To update it, you can run the following command in your terminal:" )
229- fmt .Println ("curl -sSfL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh | sh -s --" )
230- }
145+ if f , err := os .Create (historyFile ); err != nil {
146+ fmt .Printf ("Failed to save history: %v\n " , err )
147+ } else {
148+ line .WriteHistory (f )
149+ f .Close ()
150+ }
231151
232- fmt .Printf ("\n Command finished with error: %s\n " , exitErr )
233- } else {
234- fmt .Printf ("Error executing command: %v\n " , err )
152+ if err := config .SaveConfig (* appConfig ); err != nil {
153+ fmt .Println ("Failed to save configuration:" , err )
154+ } else {
155+ fmt .Println ("Cleanup confirmation has been reset. You will be prompted before cleanup commands are run." )
156+ }
157+ }
158+
159+ func runSingleCommand (appConfig * config.Config , input , llmProvider , model string ) {
160+ if input == "reset confirm" {
161+ appConfig .SkipCleanupWarning = false
162+ if err := config .SaveConfig (* appConfig ); err != nil {
163+ fmt .Println ("Failed to save configuration:" , err )
164+ } else {
165+ fmt .Println ("Cleanup confirmation has been reset. You will be prompted before cleanup commands are run." )
166+ }
167+ return
168+ }
169+
170+ // Replace 'that container' with lastContainerName if present
171+ userInput := input
172+ if strings .Contains (input , "that container" ) && appConfig .LastContainerName != "" {
173+ userInput = strings .ReplaceAll (input , "that container" , appConfig .LastContainerName )
174+ }
175+
176+ // Get running containers to provide context to the LLM
177+ var fullPrompt string
178+ psCmd := exec .Command ("sh" , "-c" , "docker ps -a --format '{{.Names}} ({{.Status}})'" )
179+ psOutput , err := psCmd .CombinedOutput ()
180+ if err == nil && len (psOutput ) > 0 {
181+ containerList := strings .TrimSpace (string (psOutput ))
182+ containerList = strings .ReplaceAll (containerList , "\n " , ", " )
183+ fullPrompt = fmt .Sprintf ("The user wants to perform a Docker command. Here is a list of all containers (running and stopped): %s.\n \n User's request: %s" , containerList , userInput )
184+ } else {
185+ fullPrompt = userInput
186+ }
187+
188+ if ! strings .Contains (userInput , "model" ) {
189+ fullPrompt += "\n \n Note: The 'docker model' command is not available on this system."
190+ }
191+
192+ response , err := llm .QueryLLM (fullPrompt , llmProvider , model )
193+ if err != nil {
194+ fmt .Printf ("Error: %v\n " , err )
195+ return
196+ }
197+
198+ if ! strings .HasPrefix (response , "docker " ) {
199+ fmt .Println (response )
200+ return
201+ }
202+
203+ // Cleanup command confirmation
204+ if isCleanupCommand (response ) && ! appConfig .SkipCleanupWarning {
205+ fmt .Printf ("WARNING: The generated command is a cleanup command:\n %s\n \n " , response )
206+
207+ // For single-command mode, we need a way to confirm.
208+ // We'll use a simple prompt here, but this could be improved.
209+ // A liner isn't running, so we use fmt.
210+ fmt .Print ("Are you sure you want to execute? [y]es, [n]o, [d]on't ask again: " )
211+ reader := bufio .NewReader (os .Stdin )
212+ answer , _ := reader .ReadString ('\n' )
213+
214+ answer = strings .ToLower (strings .TrimSpace (answer ))
215+
216+ switch answer {
217+ case "y" , "yes" :
218+ // continue to execution
219+ case "d" , "dont" , "don't ask again" :
220+ appConfig .SkipCleanupWarning = true
221+ if err := config .SaveConfig (* appConfig ); err != nil {
222+ fmt .Println ("Failed to save configuration:" , err )
235223 }
236- continue
224+ // continue to execution
225+ default :
226+ fmt .Println ("Execution cancelled." )
227+ return
237228 }
229+ }
230+
231+ fmt .Printf ("➜ executing: %s\n " , response )
232+
233+ // Execute the Docker command
234+ var stderrBuf bytes.Buffer
235+ cmd := exec .Command ("sh" , "-c" , response )
236+ cmd .Stdin = os .Stdin
237+ cmd .Stdout = os .Stdout
238+ cmd .Stderr = io .MultiWriter (os .Stderr , & stderrBuf )
239+ err = cmd .Run ()
238240
239- // Since output is streamed, we don't print it again here.
240-
241- // If the command was a 'docker run', get and store the container name
242- if strings .HasPrefix (response , "docker run" ) {
243- // With streaming, we can't just get the container ID from the output of this command.
244- // We will need a new way to get the container name.
245- // A simple approach is to list recent containers and assume the latest one is it.
246- getLatestContainerCmd := `docker ps -l -q`
247- getLatestCmd := exec .Command ("sh" , "-c" , getLatestContainerCmd )
248- latestID , latestErr := getLatestCmd .Output ()
249- if latestErr == nil && len (latestID ) > 0 {
250- containerID := strings .TrimSpace (string (latestID ))
251- inspectCmd := exec .Command ("sh" , "-c" , fmt .Sprintf ("docker inspect --format '{{.Name}}' %s | sed 's/^\\ ///'" , containerID ))
252- inspectOut , inspectErr := inspectCmd .CombinedOutput ()
253- if inspectErr == nil {
254- lastContainerName = strings .TrimSpace (string (inspectOut ))
255- fmt .Printf ("\n Container '%s' started.\n " , lastContainerName )
241+ if err != nil {
242+ if exitErr , ok := err .(* exec.ExitError ); ok {
243+ // The command exited with a non-zero status.
244+ // Stderr is already printed. We can analyze it from the buffer.
245+ stderrString := stderrBuf .String ()
246+ if strings .Contains (response , "docker model" ) {
247+ if strings .Contains (stderrString , "is not a docker command" ) {
248+ fmt .Println ("\n Error: The 'docker model' command is not available." )
249+ fmt .Println ("Please ensure you have installed the Docker Model Runner plugin." )
250+ fmt .Println ("You can try installing it with: 'sudo apt-get update && sudo apt-get install docker-model-plugin'" )
256251 }
257252 }
258-
259- statusCmd := exec .Command ("sh" , "-c" , "docker ps --format 'table {{.Names}}\t {{.Status}}\t {{.Ports}}'" )
260- statusOut , statusErr := statusCmd .CombinedOutput ()
261- if statusErr == nil {
262- fmt .Println ("\n Currently running containers:" )
263- fmt .Print (string (statusOut ))
253+ // Check for docker scout update message
254+ if strings .Contains (stderrString , "New version" ) && strings .Contains (stderrString , "available" ) {
255+ fmt .Println ("\n Hint: A new version of Docker Scout is available." )
256+ fmt .Println ("To update it, you can run the following command in your terminal:" )
257+ fmt .Println ("curl -sSfL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh | sh -s --" )
264258 }
259+ fmt .Printf ("\n Command finished with error: %s\n " , exitErr )
260+ } else {
261+ fmt .Printf ("Error executing command: %v\n " , err )
265262 }
263+ return
266264 }
267265
268- if f , err := os .Create (historyFile ); err != nil {
269- fmt .Fprintln (os .Stderr , "Error writing history file:" , err )
270- } else {
271- line .WriteHistory (f )
272- f .Close ()
266+ // If the command was a 'docker run', get and store the container name
267+ if strings .HasPrefix (response , "docker run" ) {
268+ // With streaming, we can't just get the container ID from the output of this command.
269+ // We will need a new way to get the container name.
270+ // A simple approach is to list recent containers and assume the latest one is it.
271+ getLatestContainerCmd := `docker ps -l -q`
272+ getLatestCmd := exec .Command ("sh" , "-c" , getLatestContainerCmd )
273+ latestID , latestErr := getLatestCmd .Output ()
274+ if latestErr == nil && len (latestID ) > 0 {
275+ containerID := strings .TrimSpace (string (latestID ))
276+ inspectCmd := exec .Command ("sh" , "-c" , fmt .Sprintf ("docker inspect --format '{{.Name}}' %s | sed 's/^\\ ///'" , containerID ))
277+ inspectOut , inspectErr := inspectCmd .CombinedOutput ()
278+ if inspectErr == nil {
279+ appConfig .LastContainerName = strings .TrimSpace (string (inspectOut ))
280+ fmt .Printf ("\n Container '%s' started.\n " , appConfig .LastContainerName )
281+ }
282+ }
283+
284+ statusCmd := exec .Command ("sh" , "-c" , "docker ps --format 'table {{.Names}}\t {{.Status}}\t {{.Ports}}'" )
285+ statusOut , statusErr := statusCmd .CombinedOutput ()
286+ if statusErr == nil {
287+ fmt .Println ("\n Currently running containers:" )
288+ fmt .Print (string (statusOut ))
289+ }
273290 }
274291}
0 commit comments