@@ -52,7 +52,7 @@ public struct Orchestrator: Sendable {
5252 /// dependencies); empty means every profile-enabled service.
5353 /// - Throws: `ComposeError` for unknown services, dependency cycles, or a
5454 /// dependency that fails to become healthy or complete successfully.
55- public func up( build: Bool , only services: [ String ] ) throws {
55+ public func up( build: Bool , only services: [ String ] , wait : Bool = false ) throws {
5656 try validate ( services)
5757 let selected = project. enabledServices ( explicit: services)
5858 try ensureNetworks ( )
@@ -92,6 +92,23 @@ public struct Orchestrator: Sendable {
9292 translator. runArgs ( service: name, svc, image: image, files: resolved) )
9393 }
9494 }
95+
96+ if wait { try waitUntilHealthy ( order: order, oneShot: oneShot) }
97+ }
98+
99+ /// Block until every started service that defines a healthcheck reports
100+ /// healthy. Services without a healthcheck are considered ready once created;
101+ /// one-shot (`service_completed_successfully`) services already ran to exit.
102+ private func waitUntilHealthy( order: [ String ] , oneShot: Set < String > ) throws {
103+ for name in order where !oneShot. contains ( name) {
104+ guard let svc = project. file. services [ name] , let hc = svc. healthcheck,
105+ hc. disable != true , let test = hc. test,
106+ HealthChecker . execArguments ( for: test) != nil
107+ else { continue }
108+ let cname = translator. containerName ( service: name, declared: svc. container_name)
109+ info ( " Waiting for ' \( name) ' to be healthy ... " )
110+ try HealthChecker ( runner: runner) . waitHealthy ( container: cname, health: hc)
111+ }
95112 }
96113
97114 /// Names of services that any selected service depends on with the
@@ -288,7 +305,7 @@ public struct Orchestrator: Sendable {
288305 /// one service this tails them sequentially; pass a single service to
289306 /// stream live.
290307 /// - services: limit to these services; empty means all.
291- public func logs( follow: Bool , only services: [ String ] ) throws {
308+ public func logs( follow: Bool , tail : Int ? = nil , only services: [ String ] ) throws {
292309 let selected = try select ( services) . sorted ( )
293310 if follow && selected. count > 1 {
294311 warn ( " --follow with multiple services tails them sequentially; pass one service to stream live " )
@@ -298,6 +315,7 @@ public struct Orchestrator: Sendable {
298315 let cname = translator. containerName ( service: name, declared: svc. container_name)
299316 var args = [ " logs " ]
300317 if follow { args. append ( " --follow " ) }
318+ if let tail { args += [ " -n " , String ( tail) ] }
301319 args. append ( cname)
302320 _ = try ? runner. run ( args)
303321 }
@@ -313,18 +331,25 @@ public struct Orchestrator: Sendable {
313331 /// - command: the command and arguments to run.
314332 /// - interactive: keep stdin open (`-i`).
315333 /// - tty: allocate a TTY (`-t`).
334+ /// - workdir: working directory inside the container (`--workdir`).
335+ /// - user: user to run as, `name|uid[:gid]` (`--user`).
336+ /// - env: extra `KEY=VALUE` environment entries (`--env`, repeatable).
316337 /// - Returns: the command's exit status.
317338 /// - Throws: `ComposeError.unknownService` if the service is undeclared.
318339 @discardableResult
319340 public func exec(
320- service: String , command: [ String ] , interactive: Bool = false , tty: Bool = false
341+ service: String , command: [ String ] , interactive: Bool = false , tty: Bool = false ,
342+ workdir: String ? = nil , user: String ? = nil , env: [ String ] = [ ]
321343 ) throws -> Int32 {
322344 try validate ( [ service] )
323345 let svc = project. file. services [ service]
324346 let cname = translator. containerName ( service: service, declared: svc? . container_name)
325347 var args = [ " exec " ]
326348 if interactive { args. append ( " --interactive " ) }
327349 if tty { args. append ( " --tty " ) }
350+ if let workdir { args += [ " --workdir " , workdir] }
351+ if let user { args += [ " --user " , user] }
352+ for entry in env { args += [ " --env " , entry] }
328353 args. append ( cname)
329354 args += command
330355 return try runner. run ( args)
@@ -379,6 +404,23 @@ public struct Orchestrator: Sendable {
379404 try start ( only: services)
380405 }
381406
407+ /// Send a signal (default KILL) to the selected services' containers, in
408+ /// reverse dependency order. Best-effort, like ``stop(only:)``.
409+ ///
410+ /// - Parameter signal: the signal to send (e.g. `SIGTERM`); `nil` uses the
411+ /// `container` default (KILL).
412+ public func kill( only services: [ String ] , signal: String ? = nil ) throws {
413+ for name in try ordered ( services, reversed: true ) {
414+ guard let svc = project. file. services [ name] else { continue }
415+ let cname = translator. containerName ( service: name, declared: svc. container_name)
416+ info ( " Killing \( cname) ... " )
417+ var args = [ " kill " ]
418+ if let signal { args += [ " --signal " , signal] }
419+ args. append ( cname)
420+ runner. runSilently ( args)
421+ }
422+ }
423+
382424 // MARK: - shared helpers
383425
384426 private func select( _ services: [ String ] ) throws -> Set < String > {
0 commit comments