11import type { AxFunction } from '../ai/types.js' ;
22
3+ export interface AxDockerContainer {
4+ Id : string ;
5+ Names : string [ ] ;
6+ Image : string ;
7+ ImageID : string ;
8+ Command : string ;
9+ Created : number ;
10+ State : {
11+ Status : string ;
12+ Running : boolean ;
13+ Paused : boolean ;
14+ Restarting : boolean ;
15+ OOMKilled : boolean ;
16+ Dead : boolean ;
17+ Pid : number ;
18+ ExitCode : number ;
19+ Error : string ;
20+ StartedAt : Date ;
21+ FinishedAt : Date ;
22+ } ;
23+ Status : string ;
24+ Ports : Array < {
25+ IP : string ;
26+ PrivatePort : number ;
27+ PublicPort : number ;
28+ Type : string ;
29+ } > ;
30+ Labels : { [ key : string ] : string } ;
31+ SizeRw : number ;
32+ SizeRootFs : number ;
33+ HostConfig : {
34+ NetworkMode : string ;
35+ } ;
36+ NetworkSettings : {
37+ Networks : {
38+ [ key : string ] : {
39+ IPAddress : string ;
40+ IPPrefixLen : number ;
41+ Gateway : string ;
42+ MacAddress : string ;
43+ } ;
44+ } ;
45+ } ;
46+ Mounts : Array < {
47+ Type : string ;
48+ Source : string ;
49+ Destination : string ;
50+ Mode : string ;
51+ RW : boolean ;
52+ Propagation : string ;
53+ } > ;
54+ }
55+
356export class AxDockerSession {
457 private readonly apiUrl : string ;
558 private containerId : string | null = null ;
@@ -26,31 +79,40 @@ export class AxDockerSession {
2679
2780 async createContainer ( {
2881 imageName,
29- volumes = [ ] , // Example format: [{ hostPath: '/host/path', containerPath: '/container/path' }]
30- doNotPullImage
82+ volumes = [ ] ,
83+ doNotPullImage,
84+ tag
3185 } : Readonly < {
3286 imageName : string ;
3387 volumes ?: Array < { hostPath : string ; containerPath : string } > ;
3488 doNotPullImage ?: boolean ;
89+ tag ?: string ;
3590 } > ) {
3691 const binds = volumes . map ( ( v ) => `${ v . hostPath } :${ v . containerPath } ` ) ;
3792
3893 if ( ! doNotPullImage ) {
3994 await this . pullImage ( imageName ) ;
4095 }
4196
97+ const containerConfig = {
98+ Image : imageName ,
99+ Tty : true ,
100+ OpenStdin : false ,
101+ AttachStdin : false ,
102+ AttachStdout : false ,
103+ AttachStderr : false ,
104+ HostConfig : { Binds : binds } ,
105+ Labels : { } as Record < string , string >
106+ } ;
107+
108+ if ( tag ) {
109+ containerConfig . Labels [ 'com.example.tag' ] = tag ;
110+ }
111+
42112 const response = await this . fetchDockerAPI ( `/containers/create` , {
43113 method : 'POST' ,
44114 headers : { 'Content-Type' : 'application/json' } ,
45- body : JSON . stringify ( {
46- Image : imageName ,
47- Tty : true ,
48- OpenStdin : false ,
49- AttachStdin : false ,
50- AttachStdout : false ,
51- AttachStderr : false ,
52- HostConfig : { Binds : binds }
53- } )
115+ body : JSON . stringify ( containerConfig )
54116 } ) ;
55117
56118 if ( ! response . ok ) {
@@ -63,6 +125,47 @@ export class AxDockerSession {
63125 return data ;
64126 }
65127
128+ async findOrCreateContainer ( {
129+ imageName,
130+ volumes = [ ] ,
131+ doNotPullImage,
132+ tag
133+ } : Readonly < {
134+ imageName : string ;
135+ volumes ?: Array < { hostPath : string ; containerPath : string } > ;
136+ doNotPullImage ?: boolean ;
137+ tag : string ;
138+ } > ) : Promise < { Id : string ; isNew : boolean } > {
139+ // First, try to find existing containers with the given tag
140+ const existingContainers = await this . listContainers ( true ) ;
141+ const matchingContainers = existingContainers . filter (
142+ ( container ) =>
143+ container . Labels && container . Labels [ 'com.example.tag' ] === tag
144+ ) ;
145+
146+ if ( matchingContainers && matchingContainers . length > 0 ) {
147+ // Randomly select a container from the matching ones
148+ const randomIndex = Math . floor ( Math . random ( ) * matchingContainers . length ) ;
149+ const selectedContainer = matchingContainers [ randomIndex ] ;
150+
151+ if ( selectedContainer ) {
152+ // Connect to the selected container
153+ await this . connectToContainer ( selectedContainer . Id ) ;
154+ return { Id : selectedContainer . Id , isNew : false } ;
155+ }
156+ }
157+
158+ // If no container with the tag exists, create a new one
159+ const newContainer = await this . createContainer ( {
160+ imageName,
161+ volumes,
162+ doNotPullImage,
163+ tag
164+ } ) ;
165+
166+ return { Id : newContainer . Id , isNew : true } ;
167+ }
168+
66169 async startContainer ( ) : Promise < void > {
67170 if ( ! this . containerId ) {
68171 throw new Error ( 'No container created or connected' ) ;
@@ -92,11 +195,99 @@ export class AxDockerSession {
92195 this . containerId = containerId ;
93196 }
94197
198+ async stopContainers ( {
199+ tag,
200+ remove,
201+ timeout = 10
202+ } : Readonly < { tag ?: string ; remove ?: boolean ; timeout ?: number } > ) : Promise <
203+ Array < { Id : string ; Action : 'stopped' | 'removed' } >
204+ > {
205+ const results : Array < { Id : string ; Action : 'stopped' | 'removed' } > = [ ] ;
206+
207+ // List all containers
208+ const containers = await this . listContainers ( true ) ;
209+
210+ // Filter containers by tag if provided
211+ const targetContainers = tag
212+ ? containers . filter (
213+ ( container ) => container . Labels [ 'com.example.tag' ] === tag
214+ )
215+ : containers ;
216+
217+ for ( const container of targetContainers ) {
218+ // Stop the container if it's running
219+ if ( container . State . Status === 'running' ) {
220+ const stopResponse = await this . fetchDockerAPI (
221+ `/containers/${ container . Id } /stop?t=${ timeout } ` ,
222+ { method : 'POST' }
223+ ) ;
224+
225+ if ( ! stopResponse . ok ) {
226+ console . warn (
227+ `Failed to stop container ${ container . Id } : ${ stopResponse . statusText } `
228+ ) ;
229+ continue ;
230+ }
231+
232+ results . push ( { Id : container . Id , Action : 'stopped' } ) ;
233+ }
234+
235+ // Remove the container if the remove flag is set
236+ if ( remove ) {
237+ const removeResponse = await this . fetchDockerAPI (
238+ `/containers/${ container . Id } ` ,
239+ { method : 'DELETE' }
240+ ) ;
241+
242+ if ( ! removeResponse . ok ) {
243+ console . warn (
244+ `Failed to remove container ${ container . Id } : ${ removeResponse . statusText } `
245+ ) ;
246+ continue ;
247+ }
248+
249+ results . push ( { Id : container . Id , Action : 'removed' } ) ;
250+ }
251+ }
252+
253+ return results ;
254+ }
255+
256+ async listContainers ( all : boolean = false ) : Promise < AxDockerContainer [ ] > {
257+ const response = await this . fetchDockerAPI ( `/containers/json?all=${ all } ` , {
258+ method : 'GET'
259+ } ) ;
260+ return response . json ( ) as Promise < AxDockerContainer [ ] > ;
261+ }
262+
263+ async getContainerLogs ( ) : Promise < string > {
264+ if ( ! this . containerId ) {
265+ throw new Error ( 'No container created or connected' ) ;
266+ }
267+ const response = await this . fetchDockerAPI (
268+ `/containers/${ this . containerId } /logs?stdout=true&stderr=true` ,
269+ { method : 'GET' }
270+ ) ;
271+ return response . text ( ) ;
272+ }
273+
95274 async executeCommand ( command : string ) {
275+ console . log ( 'Executing command:' , command ) ;
276+
96277 if ( ! this . containerId ) {
97278 throw new Error ( 'No container created or connected' ) ;
98279 }
99280
281+ // Check container state
282+ const containerInfo = await this . getContainerInfo ( this . containerId ) ;
283+
284+ if ( containerInfo . State . Status !== 'running' ) {
285+ await this . startContainer ( ) ;
286+
287+ // Wait for the container to be in the "running" state
288+ await this . waitForContainerToBeRunning ( this . containerId ) ;
289+ }
290+
100291 // Create exec instance
101292 const createResponse = await this . fetchDockerAPI (
102293 `/containers/${ this . containerId } /exec` ,
@@ -142,39 +333,33 @@ export class AxDockerSession {
142333 return await startResponse . text ( ) ;
143334 }
144335
145- async stopContainer ( ) {
146- if ( ! this . containerId ) {
147- throw new Error ( 'No container created or connected' ) ;
148- }
336+ // Add these new methods to the class:
149337
338+ private async getContainerInfo (
339+ containerId : string
340+ ) : Promise < AxDockerContainer > {
150341 const response = await this . fetchDockerAPI (
151- `/containers/${ this . containerId } /stop` ,
152- {
153- method : 'POST'
154- }
342+ `/containers/${ containerId } /json`
155343 ) ;
156-
157344 if ( ! response . ok ) {
158- throw new Error ( `Failed to stop container: ${ response . statusText } ` ) ;
345+ throw new Error ( `Failed to get container info : ${ response . statusText } ` ) ;
159346 }
347+ return response . json ( ) as Promise < AxDockerContainer > ;
160348 }
161349
162- async listContainers ( all : boolean = false ) {
163- const response = await this . fetchDockerAPI ( `/containers/json?all=${ all } ` , {
164- method : 'GET'
165- } ) ;
166- return response . json ( ) ;
167- }
168-
169- async getContainerLogs ( ) : Promise < string > {
170- if ( ! this . containerId ) {
171- throw new Error ( 'No container created or connected' ) ;
350+ private async waitForContainerToBeRunning (
351+ containerId : string ,
352+ timeout : number = 30000
353+ ) : Promise < void > {
354+ const startTime = Date . now ( ) ;
355+ while ( Date . now ( ) - startTime < timeout ) {
356+ const containerInfo = await this . getContainerInfo ( containerId ) ;
357+ if ( containerInfo . State . Status === 'running' ) {
358+ return ;
359+ }
360+ await new Promise ( ( resolve ) => setTimeout ( resolve , 1000 ) ) ; // Wait for 1 second before checking again
172361 }
173- const response = await this . fetchDockerAPI (
174- `/containers/${ this . containerId } /logs?stdout=true&stderr=true` ,
175- { method : 'GET' }
176- ) ;
177- return response . text ( ) ;
362+ throw new Error ( 'Timeout waiting for container to start' ) ;
178363 }
179364
180365 private async fetchDockerAPI (
@@ -202,7 +387,8 @@ export class AxDockerSession {
202387 required : [ 'command' ]
203388 } ,
204389
205- func : this . executeCommand
390+ func : async ( { command } : Readonly < { command : string } > ) =>
391+ await this . executeCommand ( command )
206392 } ;
207393 }
208394}
0 commit comments