@@ -16,37 +16,67 @@ import (
1616
1717// DevcontainerCLI is an interface for the devcontainer CLI.
1818type DevcontainerCLI interface {
19- Up (ctx context.Context ,workspaceFolder ,configPath string ,opts ... DevcontainerCLIUpOptions ) (id string ,err error )
19+ Up (ctx context.Context ,workspaceFolder ,configPath string ,opts ... DevcontainerCLIOptions ) (id string ,err error )
20+ Exec (ctx context.Context ,workspaceFolder ,configPath string ,cmd string ,cmdArgs []string ,opts ... DevcontainerCLIOptions )error
2021}
2122
22- // DevcontainerCLIUpOptions are options for the devcontainer CLI up
23- // command.
24- type DevcontainerCLIUpOptions func (* devcontainerCLIUpConfig )
23+ // DevcontainerCLIOptions are options for the devcontainer CLI commands.
24+ type DevcontainerCLIOptions func (* devcontainerCLIUpConfig )
2525
2626// WithRemoveExistingContainer is an option to remove the existing
27- // container.
28- func WithRemoveExistingContainer ()DevcontainerCLIUpOptions {
27+ // container. Can only be used with the Up command.
28+ func WithRemoveExistingContainer ()DevcontainerCLIOptions {
2929return func (o * devcontainerCLIUpConfig ) {
30- o .removeExistingContainer = true
30+ if o .command != "up" {
31+ panic ("developer error: WithRemoveExistingContainer can only be used with the Up command" )
32+ }
33+ o .args = append (o .args ,"--remove-existing-container" )
3134}
3235}
3336
34- // WithOutput sets stdout and stderr writers for Up command logs.
35- func WithOutput (stdout ,stderr io.Writer )DevcontainerCLIUpOptions {
37+ // WithOutput setsadditional stdout and stderr writers for logs.
38+ func WithOutput (stdout ,stderr io.Writer )DevcontainerCLIOptions {
3639return func (o * devcontainerCLIUpConfig ) {
3740o .stdout = stdout
3841o .stderr = stderr
3942}
4043}
4144
45+ // WithContainerID sets the container ID to target a specific container.
46+ // Can only be used with the Exec command.
47+ func WithContainerID (id string )DevcontainerCLIOptions {
48+ return func (o * devcontainerCLIUpConfig ) {
49+ if o .command != "exec" {
50+ panic ("developer error: WithContainerID can only be used with the Exec command" )
51+ }
52+ o .args = append (o .args ,"--container-id" ,id )
53+ }
54+ }
55+
56+ // WithRemoteEnv sets environment variables for the Exec command.
57+ // Can only be used with the Exec command.
58+ func WithRemoteEnv (env ... string )DevcontainerCLIOptions {
59+ return func (o * devcontainerCLIUpConfig ) {
60+ if o .command != "exec" {
61+ panic ("developer error: WithRemoteEnv can only be used with the Exec command" )
62+ }
63+ for _ ,e := range env {
64+ o .args = append (o .args ,"--remote-env" ,e )
65+ }
66+ }
67+ }
68+
4269type devcontainerCLIUpConfig struct {
70+ command string // The devcontainer CLI command to run, e.g. "up", "exec".
4371removeExistingContainer bool
72+ args []string // Additional arguments for the command.
4473stdout io.Writer
4574stderr io.Writer
4675}
4776
48- func applyDevcontainerCLIUpOptions ( opts []DevcontainerCLIUpOptions )devcontainerCLIUpConfig {
77+ func applyDevcontainerCLIOptions ( command string , opts []DevcontainerCLIOptions )devcontainerCLIUpConfig {
4978conf := devcontainerCLIUpConfig {
79+ command :command ,
5080removeExistingContainer :false ,
5181}
5282for _ ,opt := range opts {
@@ -71,8 +101,8 @@ func NewDevcontainerCLI(logger slog.Logger, execer agentexec.Execer) Devcontaine
71101}
72102}
73103
74- func (d * devcontainerCLI )Up (ctx context.Context ,workspaceFolder ,configPath string ,opts ... DevcontainerCLIUpOptions ) (string ,error ) {
75- conf := applyDevcontainerCLIUpOptions ( opts )
104+ func (d * devcontainerCLI )Up (ctx context.Context ,workspaceFolder ,configPath string ,opts ... DevcontainerCLIOptions ) (string ,error ) {
105+ conf := applyDevcontainerCLIOptions ( "up" , opts )
76106logger := d .logger .With (slog .F ("workspace_folder" ,workspaceFolder ),slog .F ("config_path" ,configPath ),slog .F ("recreate" ,conf .removeExistingContainer ))
77107
78108args := []string {
@@ -83,9 +113,7 @@ func (d *devcontainerCLI) Up(ctx context.Context, workspaceFolder, configPath st
83113if configPath != "" {
84114args = append (args ,"--config" ,configPath )
85115}
86- if conf .removeExistingContainer {
87- args = append (args ,"--remove-existing-container" )
88- }
116+ args = append (args ,conf .args ... )
89117cmd := d .execer .CommandContext (ctx ,"devcontainer" ,args ... )
90118
91119// Capture stdout for parsing and stream logs for both default and provided writers.
@@ -117,6 +145,40 @@ func (d *devcontainerCLI) Up(ctx context.Context, workspaceFolder, configPath st
117145return result .ContainerID ,nil
118146}
119147
148+ func (d * devcontainerCLI )Exec (ctx context.Context ,workspaceFolder ,configPath string ,cmd string ,cmdArgs []string ,opts ... DevcontainerCLIOptions )error {
149+ conf := applyDevcontainerCLIOptions ("exec" ,opts )
150+ logger := d .logger .With (slog .F ("workspace_folder" ,workspaceFolder ),slog .F ("config_path" ,configPath ))
151+
152+ args := []string {"exec" }
153+ if workspaceFolder != "" {
154+ args = append (args ,"--workspace-folder" ,workspaceFolder )
155+ }
156+ if configPath != "" {
157+ args = append (args ,"--config" ,configPath )
158+ }
159+ args = append (args ,conf .args ... )
160+ args = append (args ,cmd )
161+ args = append (args ,cmdArgs ... )
162+ c := d .execer .CommandContext (ctx ,"devcontainer" ,args ... )
163+
164+ stdoutWriters := []io.Writer {& devcontainerCLILogWriter {ctx :ctx ,logger :logger .With (slog .F ("stdout" ,true ))}}
165+ if conf .stdout != nil {
166+ stdoutWriters = append (stdoutWriters ,conf .stdout )
167+ }
168+ c .Stdout = io .MultiWriter (stdoutWriters ... )
169+ stderrWriters := []io.Writer {& devcontainerCLILogWriter {ctx :ctx ,logger :logger .With (slog .F ("stderr" ,true ))}}
170+ if conf .stderr != nil {
171+ stderrWriters = append (stderrWriters ,conf .stderr )
172+ }
173+ c .Stderr = io .MultiWriter (stderrWriters ... )
174+
175+ if err := c .Run ();err != nil {
176+ return xerrors .Errorf ("devcontainer exec failed: %w" ,err )
177+ }
178+
179+ return nil
180+ }
181+
120182// parseDevcontainerCLILastLine parses the last line of the devcontainer CLI output
121183// which is a JSON object.
122184func parseDevcontainerCLILastLine (ctx context.Context ,logger slog.Logger ,p []byte ) (result devcontainerCLIResult ,err error ) {