|
| 1 | +package cli |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "encoding/hex" |
| 6 | + "encoding/json" |
| 7 | + "fmt" |
| 8 | + "os" |
| 9 | + "os/exec" |
| 10 | + "path/filepath" |
| 11 | + |
| 12 | + "github.com/devcontainers/cli/internal/config" |
| 13 | + "github.com/spf13/cobra" |
| 14 | +) |
| 15 | + |
| 16 | +type openOpts struct { |
| 17 | + workspaceFolder string |
| 18 | + configPath string |
| 19 | + editor string |
| 20 | + dryRun bool |
| 21 | +} |
| 22 | + |
| 23 | +// fileURIJSON is VS Code's serialized `file:` URI for the dev container config, |
| 24 | +// as embedded in the dev-container remote authority. On Linux there is no |
| 25 | +// authority component (it only appears for WSL/UNC paths on Windows). |
| 26 | +type fileURIJSON struct { |
| 27 | + Scheme string `json:"scheme"` |
| 28 | + Authority string `json:"authority,omitempty"` |
| 29 | + Path string `json:"path"` |
| 30 | +} |
| 31 | + |
| 32 | +// devcontainerURIJSON is the JSON payload hex-encoded into the |
| 33 | +// `dev-container+<hex>` remote authority. Field names/shape match what the VS |
| 34 | +// Code Dev Containers resolver expects (verified against the vscli launcher). |
| 35 | +type devcontainerURIJSON struct { |
| 36 | + HostPath string `json:"hostPath"` |
| 37 | + ConfigFile fileURIJSON `json:"configFile"` |
| 38 | +} |
| 39 | + |
| 40 | +func newOpenCmd() *cobra.Command { |
| 41 | + var opts openOpts |
| 42 | + |
| 43 | + cmd := &cobra.Command{ |
| 44 | + Use: "open [path]", |
| 45 | + Short: "Open a workspace in VS Code, attached to its dev container", |
| 46 | + Long: `Open a workspace folder in VS Code inside its dev container. |
| 47 | +
|
| 48 | +This builds the vscode-remote:// dev-container URI for the workspace and launches |
| 49 | +the editor with it. VS Code's Dev Containers extension then provisions (or |
| 50 | +reconnects to) the container — so 'open' pairs with 'up': run 'devcontainer up' |
| 51 | +first to provision, then 'devcontainer open' to attach the editor. |
| 52 | +
|
| 53 | +Go-only command; not part of the upstream @devcontainers/cli.`, |
| 54 | + Args: cobra.MaximumNArgs(1), |
| 55 | + RunE: func(cmd *cobra.Command, args []string) error { |
| 56 | + if len(args) > 0 && opts.workspaceFolder == "" { |
| 57 | + opts.workspaceFolder = args[0] |
| 58 | + } |
| 59 | + return runOpen(cmd.Context(), outputFor(cmd), opts) |
| 60 | + }, |
| 61 | + } |
| 62 | + |
| 63 | + f := cmd.Flags() |
| 64 | + f.StringVar(&opts.workspaceFolder, "workspace-folder", "", "Workspace folder path (defaults to [path] or the current directory).") |
| 65 | + f.StringVar(&opts.configPath, "config", "", "devcontainer.json path.") |
| 66 | + f.StringVar(&opts.editor, "editor", "code", "Editor launcher binary (e.g. code, code-insiders, cursor).") |
| 67 | + f.BoolVar(&opts.dryRun, "dry-run", false, "Print the folder URI and launch command without opening the editor.") |
| 68 | + |
| 69 | + return cmd |
| 70 | +} |
| 71 | + |
| 72 | +func runOpen(ctx context.Context, out Output, opts openOpts) error { |
| 73 | + workspaceFolder := opts.workspaceFolder |
| 74 | + if workspaceFolder == "" { |
| 75 | + workspaceFolder, _ = os.Getwd() |
| 76 | + } |
| 77 | + workspaceFolder = resolvePath(workspaceFolder) |
| 78 | + |
| 79 | + configPath := "" |
| 80 | + if opts.configPath != "" { |
| 81 | + configPath = resolvePath(opts.configPath) |
| 82 | + } |
| 83 | + |
| 84 | + // Load the config to discover the resolved devcontainer.json path and the |
| 85 | + // workspace folder *inside* the container (the URI's trailing path). |
| 86 | + loadResult, err := config.LoadDevContainerConfig(workspaceFolder, configPath, "") |
| 87 | + if err != nil { |
| 88 | + return err |
| 89 | + } |
| 90 | + |
| 91 | + localConfigPath := loadResult.Config.ConfigFilePath |
| 92 | + containerFolder := "" |
| 93 | + if loadResult.WorkspaceConfig != nil { |
| 94 | + containerFolder = loadResult.WorkspaceConfig.WorkspaceFolder |
| 95 | + } |
| 96 | + if containerFolder == "" { |
| 97 | + return fmt.Errorf("could not determine the container workspace folder for %s", workspaceFolder) |
| 98 | + } |
| 99 | + |
| 100 | + uri, err := devcontainerFolderURI(workspaceFolder, localConfigPath, containerFolder) |
| 101 | + if err != nil { |
| 102 | + return err |
| 103 | + } |
| 104 | + |
| 105 | + editor := opts.editor |
| 106 | + if editor == "" { |
| 107 | + editor = "code" |
| 108 | + } |
| 109 | + |
| 110 | + if opts.dryRun { |
| 111 | + fmt.Fprintln(out.Stdout(), uri) |
| 112 | + fmt.Fprintf(out.Stderr(), "%s --folder-uri %s\n", editor, uri) |
| 113 | + return nil |
| 114 | + } |
| 115 | + |
| 116 | + fmt.Fprintf(out.Stderr(), "Opening %s in VS Code (dev container)...\n", workspaceFolder) |
| 117 | + launch := exec.CommandContext(ctx, editor, "--folder-uri", uri) |
| 118 | + launch.Stdout = out.Stdout() |
| 119 | + launch.Stderr = out.Stderr() |
| 120 | + if err := launch.Run(); err != nil { |
| 121 | + return fmt.Errorf("launch %q: %w (is the editor's launcher on PATH? override with --editor)", editor, err) |
| 122 | + } |
| 123 | + return nil |
| 124 | +} |
| 125 | + |
| 126 | +// devcontainerFolderURI builds the vscode-remote:// folder URI that opens |
| 127 | +// containerFolder inside the dev container for the given local workspace and |
| 128 | +// config. The remote authority is `dev-container+<hex>` where <hex> is the |
| 129 | +// hex-encoded JSON {hostPath, configFile:{scheme,path}}. |
| 130 | +func devcontainerFolderURI(localWorkspaceFolder, localConfigPath, containerFolder string) (string, error) { |
| 131 | + payload := devcontainerURIJSON{ |
| 132 | + HostPath: localWorkspaceFolder, |
| 133 | + ConfigFile: fileURIJSON{ |
| 134 | + Scheme: "file", |
| 135 | + // Linux fs paths already use forward slashes; use the absolute path as-is. |
| 136 | + Path: filepath.ToSlash(localConfigPath), |
| 137 | + }, |
| 138 | + } |
| 139 | + data, err := json.Marshal(payload) |
| 140 | + if err != nil { |
| 141 | + return "", err |
| 142 | + } |
| 143 | + authority := "dev-container+" + hex.EncodeToString(data) |
| 144 | + return "vscode-remote://" + authority + containerFolder, nil |
| 145 | +} |
0 commit comments