From adca58a9b4455109cd34234d6ee101ef6dcdfc18 Mon Sep 17 00:00:00 2001 From: Stuart Leeks Date: Fri, 6 Feb 2026 16:19:15 +0000 Subject: [PATCH 1/2] Add support for opening subfolders in VS Code devcontainer commands --- cmd/devcontainerx/openincode.go | 43 +++++++++++++++++++--- internal/pkg/devcontainers/devcontainer.go | 35 ++++++++++++++++++ internal/pkg/devcontainers/remoteuri.go | 6 ++- 3 files changed, 78 insertions(+), 6 deletions(-) diff --git a/cmd/devcontainerx/openincode.go b/cmd/devcontainerx/openincode.go index 23d431b..2978a16 100644 --- a/cmd/devcontainerx/openincode.go +++ b/cmd/devcontainerx/openincode.go @@ -3,6 +3,8 @@ package main import ( "fmt" "os/exec" + "path/filepath" + "strings" "github.com/spf13/cobra" "github.com/stuartleeks/devcontainer-cli/internal/pkg/devcontainers" @@ -10,38 +12,69 @@ import ( ) func createOpenInCodeCommand() *cobra.Command { + var subFolder string cmd := &cobra.Command{ Use: "open-in-code ", Short: "open the specified path devcontainer project in VS Code", Long: "Open the specified path (containing a .devcontainer folder in VS Code", RunE: func(cmd *cobra.Command, args []string) error { - return launchDevContainer(cmd, "code", args) + return launchDevContainer(cmd, "code", args, subFolder) }, } + cmd.Flags().StringVarP(&subFolder, "sub-folder", "s", "", "sub-folder within the devcontainer to open") return cmd } func createOpenInCodeInsidersCommand() *cobra.Command { + var subFolder string cmd := &cobra.Command{ Use: "open-in-code-insiders ", Short: "open the specified path devcontainer project in VS Code Insiders", Long: "Open the specified path (containing a .devcontainer folder in VS Code Insiders", RunE: func(cmd *cobra.Command, args []string) error { - return launchDevContainer(cmd, "code-insiders", args) + return launchDevContainer(cmd, "code-insiders", args, subFolder) }, } + cmd.Flags().StringVarP(&subFolder, "sub-folder", "s", "", "sub-folder within the devcontainer to open") return cmd } -func launchDevContainer(cmd *cobra.Command, appBase string, args []string) error { +func launchDevContainer(cmd *cobra.Command, appBase string, args []string, subFolder string) error { if len(args) > 1 { return cmd.Usage() } path := "." // default to current directory - if len(args) == 1 { + if len(args) >= 1 { path = args[0] } - launchURI, err := devcontainers.GetDevContainerURI(path) + // allow the command to be invoked from a subfolder of the folder containing the devcontainer.json by searching ancestor paths for a devcontainer.json + devcontainerPath, err := devcontainers.FindDevContainerInAncestorPaths(path) + if err != nil { + return fmt.Errorf("error finding devcontainer.json: %s", err) + } + fmt.Printf("Found devcontainer.json at %s\n", devcontainerPath) + + // If subFolder is not empty then use that as the path to open in VS Code (note it should be relative to the folder containing the devcontainer.json/.devcontainer folder) + // If subFolder is empty and the current folder is contained within the folder containing the devcontainer.json, then open the current folder in VS Code, otherwise open the folder containing the devcontainer.json in VS Code + if subFolder == "" { + absDevContainerPath, err := filepath.Abs(devcontainerPath) + if err != nil { + return fmt.Errorf("error getting absolute path: %s", err) + } + absCurrentPath, err := filepath.Abs(path) + if err != nil { + return fmt.Errorf("error getting absolute path: %s", err) + } + if absCurrentPath != absDevContainerPath && strings.HasPrefix(absCurrentPath, absDevContainerPath) { + subFolder, err = filepath.Rel(absDevContainerPath, absCurrentPath) + if err != nil { + return fmt.Errorf("error getting relative path: %s", err) + } + fmt.Printf("Opening subfolder %s within devcontainer\n", subFolder) + } + } + + launchURI, err := devcontainers.GetDevContainerURI(devcontainerPath, subFolder) if err != nil { return err } diff --git a/internal/pkg/devcontainers/devcontainer.go b/internal/pkg/devcontainers/devcontainer.go index c7c7fe4..405732d 100644 --- a/internal/pkg/devcontainers/devcontainer.go +++ b/internal/pkg/devcontainers/devcontainer.go @@ -20,3 +20,38 @@ func getDevContainerJsonPath(folderPath string) (string, error) { return "", fmt.Errorf("devcontainer.json not found. Looked for %s", strings.Join(pathsToTest, ",")) } + +func FindDevContainerInAncestorPaths(folderPath string) (string, error) { + currentPath, err := filepath.Abs(folderPath) + if err != nil { + return "", fmt.Errorf("error getting absolute path: %w", err) + } + + for { + // Check if devcontainer.json exists in current path + _, err := getDevContainerJsonPath(currentPath) + if err == nil { + return currentPath, nil + } + + // Check if this is a git repository root + gitPath := filepath.Join(currentPath, ".git") + gitInfo, gitErr := os.Stat(gitPath) + isGitRoot := gitErr == nil && gitInfo.IsDir() + + // If we're at a git root, stop searching (we already checked this folder) + if isGitRoot { + return "", fmt.Errorf("devcontainer.json not found in ancestor paths") + } + + // Move to parent directory + parentPath := filepath.Dir(currentPath) + + // Check if we've reached the root (parent is same as current) + if parentPath == currentPath { + return "", fmt.Errorf("devcontainer.json not found in ancestor paths") + } + + currentPath = parentPath + } +} diff --git a/internal/pkg/devcontainers/remoteuri.go b/internal/pkg/devcontainers/remoteuri.go index 2ad6397..9b04fb6 100644 --- a/internal/pkg/devcontainers/remoteuri.go +++ b/internal/pkg/devcontainers/remoteuri.go @@ -12,7 +12,8 @@ import ( ) // GetDevContainerURI gets the devcontainer URI for a folder to launch using the VS Code --folder-uri switch -func GetDevContainerURI(folderPath string) (string, error) { +// If subFolder is specified, it is appended to the workspaceMountPath +func GetDevContainerURI(folderPath string, subFolder string) (string, error) { absPath, err := filepath.Abs(folderPath) if err != nil { @@ -33,6 +34,9 @@ func GetDevContainerURI(folderPath string) (string, error) { if err != nil { return "", err } + if subFolder != "" { + workspaceMountPath = filepath.Join(workspaceMountPath, subFolder) + } uri := fmt.Sprintf("vscode-remote://dev-container+%s%s", launchPathHex, workspaceMountPath) return uri, nil From 2dc8baae29d0e9420266837af8f814aebced26f3 Mon Sep 17 00:00:00 2001 From: Stuart Leeks Date: Fri, 6 Feb 2026 16:47:13 +0000 Subject: [PATCH 2/2] Update docs --- docs/open-in-code.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/open-in-code.md b/docs/open-in-code.md index 14fd80e..bc36ca6 100644 --- a/docs/open-in-code.md +++ b/docs/open-in-code.md @@ -2,6 +2,11 @@ The `devcontainer open-in-code` command opens the current folder in VS Code as a dev container, i.e. it skips the normal step of opening in VS Code and then clicking # on the "Re-open in container" prompt to reload the window as a dev container. With `devcontainer open-in-code` you get straight to the dev container! -You can also use `devcontainer open-in-code ` to open a different folder as a devcontainer. +You can also use `devcontainer open-in-code ` to open a the devcontainer from a different folder. + +The command also supports the `--sub-folder` option. This allows you to specify a sub-folder within the folder that contains the devcontainer configuration (i.e. with the `.devcontainer` folder or `.devcontainer.json` file). + +The `open-in-code` command will walk up the tree from the current/specified folder until it finds a folder with a devcontainer configuration, and then open that folder in VS Code as a dev container. +If `--sub-folder` is not specified and the devcontainer configuration is found in a parent folder, relative path to the current folder is used as the sub-folder. If you want to use the VS Code Insiders release, you can use `devcontainer open-in-code-insiders`. \ No newline at end of file