Skip to content

Commit a218cca

Browse files
authored
Merge pull request #19 from stuartleeks/sl/open-in-code
Add open-in-code commands
2 parents 4113a0b + d31d473 commit a218cca

File tree

9 files changed

+239
-3
lines changed

9 files changed

+239
-3
lines changed

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,7 @@ endif
2121
--workdir "${PWD}" \
2222
devcontainer-cli \
2323
-c "${PWD}/scripts/ci_release.sh"
24+
25+
26+
test:
27+
go test -v ./...

cmd/devcontainer/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ func main() {
2828
rootCmd.AddCommand(createListCommand())
2929
rootCmd.AddCommand(createTemplateCommand())
3030
rootCmd.AddCommand(createUpdateCommand())
31+
rootCmd.AddCommand(createOpenInCodeCommand())
32+
rootCmd.AddCommand(createOpenInCodeInsidersCommand())
3133
rootCmd.AddCommand(createVersionCommand())
3234

3335
_ = rootCmd.Execute()

cmd/devcontainer/openincode.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os/exec"
6+
7+
"github.com/spf13/cobra"
8+
"github.com/stuartleeks/devcontainer-cli/internal/pkg/devcontainers"
9+
"github.com/stuartleeks/devcontainer-cli/internal/pkg/wsl"
10+
)
11+
12+
func createOpenInCodeCommand() *cobra.Command {
13+
cmd := &cobra.Command{
14+
Use: "open-in-code <path>",
15+
Short: "open the specified path devcontainer project in VS Code",
16+
Long: "Open the specified path (containing a .devcontainer folder in VS Code",
17+
RunE: func(cmd *cobra.Command, args []string) error {
18+
return launchDevContainer(cmd, "code", args)
19+
},
20+
}
21+
return cmd
22+
}
23+
func createOpenInCodeInsidersCommand() *cobra.Command {
24+
cmd := &cobra.Command{
25+
Use: "open-in-code-insiders <path>",
26+
Short: "open the specified path devcontainer project in VS Code Insiders",
27+
Long: "Open the specified path (containing a .devcontainer folder in VS Code Insiders",
28+
RunE: func(cmd *cobra.Command, args []string) error {
29+
return launchDevContainer(cmd, "code-insiders", args)
30+
},
31+
}
32+
return cmd
33+
}
34+
35+
func launchDevContainer(cmd *cobra.Command, appBase string, args []string) error {
36+
if len(args) != 1 {
37+
return cmd.Usage()
38+
}
39+
path := args[0]
40+
41+
launchURI, err := devcontainers.GetDevContainerURI(path)
42+
if err != nil {
43+
return err
44+
}
45+
var execCmd *exec.Cmd
46+
if wsl.IsWsl() {
47+
execCmd = exec.Command("cmd.exe", "/C", appBase+".cmd", "--folder-uri="+launchURI)
48+
} else {
49+
execCmd = exec.Command(appBase, "--folder-uri="+launchURI)
50+
}
51+
output, err := execCmd.Output()
52+
fmt.Println(string(output))
53+
if err != nil {
54+
return err
55+
}
56+
return nil
57+
}

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ require (
77
github.com/rhysd/go-github-selfupdate v1.2.2
88
github.com/spf13/cobra v1.0.0
99
github.com/spf13/viper v1.4.0
10+
github.com/nats-io/nats.go v1.8.1
11+
github.com/stretchr/testify v1.2.2
1012
)

go.sum

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er
3535
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
3636
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
3737
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
38+
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
3839
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
3940
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
4041
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -48,6 +49,7 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
4849
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
4950
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
5051
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
52+
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
5153
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
5254
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
5355
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
@@ -71,8 +73,13 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk
7173
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
7274
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
7375
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
76+
github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ3M8LwxM=
77+
github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4=
78+
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
7479
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
80+
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
7581
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
82+
github.com/onsi/gomega v1.4.2 h1:3mYCb7aPxS/RU7TI1y4rkEn1oKmPRjNJLNEXgw7MH2I=
7683
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
7784
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
7885
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
@@ -124,6 +131,7 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
124131
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
125132
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
126133
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
134+
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
127135
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
128136
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
129137
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -156,6 +164,7 @@ golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGm
156164
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
157165
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
158166
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
167+
google.golang.org/appengine v1.3.0 h1:FBSsiFRMz3LBeXIomRnVzrQwSDj4ibvcRexLG0LZGQk=
159168
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
160169
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
161170
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@@ -164,8 +173,10 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
164173
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
165174
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
166175
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
176+
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
167177
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
168178
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
179+
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
169180
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
170181
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
171182
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

internal/pkg/config/config.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package config
33
import (
44
"fmt"
55
"os"
6+
"path/filepath"
67
"time"
78

89
"github.com/spf13/viper"
@@ -33,11 +34,14 @@ func EnsureInitialised() {
3334
}
3435
}
3536
func getConfigPath() string {
37+
var path string
3638
if os.Getenv("HOME") != "" {
37-
return "$HOME/.devcontainer-cli/"
39+
path = filepath.Join("$HOME", ".devcontainer-cli/")
40+
} else {
41+
// if HOME not set, assume Windows and use USERPROFILE env var
42+
path = filepath.Join("$USERPROFILE", ".devcontainer-cli/")
3843
}
39-
// if HOME not set, assume Windows and use USERPROFILE env var
40-
return "$USERPROFILE/.devcontainer-cli/"
44+
return os.ExpandEnv(path)
4145
}
4246

4347
func GetTemplateFolders() []string {
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package devcontainers
2+
3+
import (
4+
"encoding/hex"
5+
"fmt"
6+
"io/ioutil"
7+
"path/filepath"
8+
"regexp"
9+
10+
"github.com/stuartleeks/devcontainer-cli/internal/pkg/wsl"
11+
)
12+
13+
// GetDevContainerURI gets the devcontainer URI for a folder to launch using the VS Code --folder-uri switch
14+
func GetDevContainerURI(folderPath string) (string, error) {
15+
16+
absPath, err := filepath.Abs(folderPath)
17+
if err != nil {
18+
return "", fmt.Errorf("Error handling path %q: %s", folderPath, err)
19+
}
20+
21+
launchPath := absPath
22+
if wsl.IsWsl() {
23+
var err error
24+
launchPath, err = wsl.ConvertWslPathToWindowsPath(launchPath)
25+
if err != nil {
26+
return "", err
27+
}
28+
}
29+
30+
launchPathHex := convertToHexString(launchPath)
31+
workspaceMountPath, err := getWorkspaceMountPath(absPath)
32+
if err != nil {
33+
return "", err
34+
}
35+
uri := fmt.Sprintf("vscode-remote://dev-container+%s%s", launchPathHex, workspaceMountPath)
36+
37+
return uri, nil
38+
}
39+
40+
func convertToHexString(input string) string {
41+
return hex.EncodeToString([]byte(input))
42+
}
43+
44+
// TODO: add tests (and implementation) to handle JSON parsing with comments
45+
// Current implementation doesn't handle
46+
// - block comments
47+
// - the value split on a new line from the property name
48+
49+
func getWorkspaceMountPath(folderPath string) (string, error) {
50+
// TODO - consider how to support repository-containers (https://github.com/microsoft/vscode-remote-release/issues/3218)
51+
52+
devcontainerDefinitionPath := filepath.Join(folderPath, ".devcontainer/devcontainer.json")
53+
buf, err := ioutil.ReadFile(devcontainerDefinitionPath)
54+
if err != nil {
55+
return "", fmt.Errorf("Error loading devcontainer definition: %s", err)
56+
}
57+
58+
workspaceMountPath, err := getWorkspaceMountPathFromDevcontainerDefinition(buf)
59+
if err != nil {
60+
return "", fmt.Errorf("Error parsing devcontainer definition: %s", err)
61+
}
62+
if workspaceMountPath != "" {
63+
return workspaceMountPath, nil
64+
}
65+
66+
// No `workspaceFolder` found in devcontainer.json - use default
67+
_, folderName := filepath.Split(folderPath)
68+
return fmt.Sprintf("/workspaces/%s", folderName), nil
69+
}
70+
71+
func getWorkspaceMountPathFromDevcontainerDefinition(definition []byte) (string, error) {
72+
r, err := regexp.Compile("(?m)^\\s*\"workspaceFolder\"\\s*:\\s*\"(.*)\"")
73+
if err != nil {
74+
return "", fmt.Errorf("Error compiling regex: %s", err)
75+
}
76+
matches := r.FindSubmatch(definition)
77+
if len(matches) == 2 {
78+
return string(matches[1]), nil
79+
}
80+
return "", nil
81+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package devcontainers
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestHexToString(t *testing.T) {
10+
input := "\\\\wsl$\\Ubuntusl\\home\\stuart\\source\\kips-operator"
11+
expected := "5c5c77736c245c5562756e7475736c5c686f6d655c7374756172745c736f757263655c6b6970732d6f70657261746f72"
12+
actual := convertToHexString(input)
13+
assert.Equal(t, expected, actual)
14+
}
15+
16+
func TestGetWorkspaceFolder_withWorkspaceFolder(t *testing.T) {
17+
18+
content := `{
19+
"someProp": 2,
20+
// add a content here for good measure
21+
"workspaceFolder": "/workspace/wibble",
22+
}`
23+
result, err := getWorkspaceMountPathFromDevcontainerDefinition([]byte(content))
24+
25+
assert.NoError(t, err)
26+
assert.Equal(t, "/workspace/wibble", result)
27+
}
28+
func TestGetWorkspaceFolder_withCommentedWorkspaceFolder(t *testing.T) {
29+
30+
content := `{
31+
"someProp": 2,
32+
// add a content here for good measure
33+
//"workspaceFolder": "/workspace/wibble",
34+
}`
35+
result, err := getWorkspaceMountPathFromDevcontainerDefinition([]byte(content))
36+
37+
assert.NoError(t, err)
38+
assert.Equal(t, "", result)
39+
}
40+
func TestGetWorkspaceFolder_withNoWorkspaceFolder(t *testing.T) {
41+
42+
content := `{
43+
"someProp": 2,
44+
// add a content here for good measure
45+
}`
46+
result, err := getWorkspaceMountPathFromDevcontainerDefinition([]byte(content))
47+
48+
assert.NoError(t, err)
49+
assert.Equal(t, "", result)
50+
}

internal/pkg/wsl/wsl.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package wsl
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"strings"
8+
)
9+
10+
// IsWsl returns true if running under WSL
11+
func IsWsl() bool {
12+
_, exists := os.LookupEnv("WSL_DISTRO_NAME")
13+
return exists
14+
}
15+
16+
// ConvertWslPathToWindowsPath converts a WSL path to the corresponding \\wsl$\... path for access from Windows
17+
func ConvertWslPathToWindowsPath(path string) (string, error) {
18+
cmd := exec.Command("wslpath", "-w", path)
19+
20+
buf, err := cmd.Output()
21+
if err != nil {
22+
return "", fmt.Errorf("Error running wslpath: %s", err)
23+
}
24+
return strings.TrimSpace(string(buf)), nil
25+
}

0 commit comments

Comments
 (0)