@@ -17,38 +17,83 @@ import (
1717// DevcontainerCLI is an interface for the devcontainer CLI.
1818type DevcontainerCLI interface {
1919Up (ctx context.Context ,workspaceFolder ,configPath string ,opts ... DevcontainerCLIUpOptions ) (id string ,err error )
20+ Exec (ctx context.Context ,workspaceFolder ,configPath string ,cmd string ,cmdArgs []string ,opts ... DevcontainerCLIExecOptions )error
2021}
2122
22- // DevcontainerCLIUpOptions are options for the devcontainer CLIup
23+ // DevcontainerCLIUpOptions are options for the devcontainer CLIUp
2324// command.
2425type DevcontainerCLIUpOptions func (* devcontainerCLIUpConfig )
2526
27+ type devcontainerCLIUpConfig struct {
28+ args []string // Additional arguments for the Up command.
29+ stdout io.Writer
30+ stderr io.Writer
31+ }
32+
2633// WithRemoveExistingContainer is an option to remove the existing
2734// container.
2835func WithRemoveExistingContainer ()DevcontainerCLIUpOptions {
2936return func (o * devcontainerCLIUpConfig ) {
30- o .removeExistingContainer = true
37+ o .args = append ( o . args , "--remove-existing-container" )
3138}
3239}
3340
34- // WithOutput sets stdout and stderr writers for Up command logs.
35- func WithOutput (stdout ,stderr io.Writer )DevcontainerCLIUpOptions {
41+ // WithUpOutput sets additional stdout and stderr writers for logs
42+ // during Up operations.
43+ func WithUpOutput (stdout ,stderr io.Writer )DevcontainerCLIUpOptions {
3644return func (o * devcontainerCLIUpConfig ) {
3745o .stdout = stdout
3846o .stderr = stderr
3947}
4048}
4149
42- type devcontainerCLIUpConfig struct {
43- removeExistingContainer bool
44- stdout io.Writer
45- stderr io.Writer
50+ // DevcontainerCLIExecOptions are options for the devcontainer CLI Exec
51+ // command.
52+ type DevcontainerCLIExecOptions func (* devcontainerCLIExecConfig )
53+
54+ type devcontainerCLIExecConfig struct {
55+ args []string // Additional arguments for the Exec command.
56+ stdout io.Writer
57+ stderr io.Writer
58+ }
59+
60+ // WithExecOutput sets additional stdout and stderr writers for logs
61+ // during Exec operations.
62+ func WithExecOutput (stdout ,stderr io.Writer )DevcontainerCLIExecOptions {
63+ return func (o * devcontainerCLIExecConfig ) {
64+ o .stdout = stdout
65+ o .stderr = stderr
66+ }
67+ }
68+
69+ // WithContainerID sets the container ID to target a specific container.
70+ func WithContainerID (id string )DevcontainerCLIExecOptions {
71+ return func (o * devcontainerCLIExecConfig ) {
72+ o .args = append (o .args ,"--container-id" ,id )
73+ }
74+ }
75+
76+ // WithRemoteEnv sets environment variables for the Exec command.
77+ func WithRemoteEnv (env ... string )DevcontainerCLIExecOptions {
78+ return func (o * devcontainerCLIExecConfig ) {
79+ for _ ,e := range env {
80+ o .args = append (o .args ,"--remote-env" ,e )
81+ }
82+ }
4683}
4784
4885func applyDevcontainerCLIUpOptions (opts []DevcontainerCLIUpOptions )devcontainerCLIUpConfig {
49- conf := devcontainerCLIUpConfig {
50- removeExistingContainer :false ,
86+ conf := devcontainerCLIUpConfig {}
87+ for _ ,opt := range opts {
88+ if opt != nil {
89+ opt (& conf )
90+ }
5191}
92+ return conf
93+ }
94+
95+ func applyDevcontainerCLIExecOptions (opts []DevcontainerCLIExecOptions )devcontainerCLIExecConfig {
96+ conf := devcontainerCLIExecConfig {}
5297for _ ,opt := range opts {
5398if opt != nil {
5499opt (& conf )
@@ -73,7 +118,7 @@ func NewDevcontainerCLI(logger slog.Logger, execer agentexec.Execer) Devcontaine
73118
74119func (d * devcontainerCLI )Up (ctx context.Context ,workspaceFolder ,configPath string ,opts ... DevcontainerCLIUpOptions ) (string ,error ) {
75120conf := applyDevcontainerCLIUpOptions (opts )
76- logger := d .logger .With (slog .F ("workspace_folder" ,workspaceFolder ),slog .F ("config_path" ,configPath ), slog . F ( "recreate" , conf . removeExistingContainer ) )
121+ logger := d .logger .With (slog .F ("workspace_folder" ,workspaceFolder ),slog .F ("config_path" ,configPath ))
77122
78123args := []string {
79124"up" ,
@@ -83,9 +128,7 @@ func (d *devcontainerCLI) Up(ctx context.Context, workspaceFolder, configPath st
83128if configPath != "" {
84129args = append (args ,"--config" ,configPath )
85130}
86- if conf .removeExistingContainer {
87- args = append (args ,"--remove-existing-container" )
88- }
131+ args = append (args ,conf .args ... )
89132cmd := d .execer .CommandContext (ctx ,"devcontainer" ,args ... )
90133
91134// Capture stdout for parsing and stream logs for both default and provided writers.
@@ -117,6 +160,40 @@ func (d *devcontainerCLI) Up(ctx context.Context, workspaceFolder, configPath st
117160return result .ContainerID ,nil
118161}
119162
163+ func (d * devcontainerCLI )Exec (ctx context.Context ,workspaceFolder ,configPath string ,cmd string ,cmdArgs []string ,opts ... DevcontainerCLIExecOptions )error {
164+ conf := applyDevcontainerCLIExecOptions (opts )
165+ logger := d .logger .With (slog .F ("workspace_folder" ,workspaceFolder ),slog .F ("config_path" ,configPath ))
166+
167+ args := []string {"exec" }
168+ if workspaceFolder != "" {
169+ args = append (args ,"--workspace-folder" ,workspaceFolder )
170+ }
171+ if configPath != "" {
172+ args = append (args ,"--config" ,configPath )
173+ }
174+ args = append (args ,conf .args ... )
175+ args = append (args ,cmd )
176+ args = append (args ,cmdArgs ... )
177+ c := d .execer .CommandContext (ctx ,"devcontainer" ,args ... )
178+
179+ stdoutWriters := []io.Writer {& devcontainerCLILogWriter {ctx :ctx ,logger :logger .With (slog .F ("stdout" ,true ))}}
180+ if conf .stdout != nil {
181+ stdoutWriters = append (stdoutWriters ,conf .stdout )
182+ }
183+ c .Stdout = io .MultiWriter (stdoutWriters ... )
184+ stderrWriters := []io.Writer {& devcontainerCLILogWriter {ctx :ctx ,logger :logger .With (slog .F ("stderr" ,true ))}}
185+ if conf .stderr != nil {
186+ stderrWriters = append (stderrWriters ,conf .stderr )
187+ }
188+ c .Stderr = io .MultiWriter (stderrWriters ... )
189+
190+ if err := c .Run ();err != nil {
191+ return xerrors .Errorf ("devcontainer exec failed: %w" ,err )
192+ }
193+
194+ return nil
195+ }
196+
120197// parseDevcontainerCLILastLine parses the last line of the devcontainer CLI output
121198// which is a JSON object.
122199func parseDevcontainerCLILastLine (ctx context.Context ,logger slog.Logger ,p []byte ) (result devcontainerCLIResult ,err error ) {