4
4
"context"
5
5
"fmt"
6
6
"net/url"
7
+ "path/filepath"
7
8
"strings"
8
9
9
10
"github.com/skratchdot/open-golang/open"
@@ -28,111 +29,174 @@ func (r *RootCmd) open() *clibase.Cmd {
28
29
return cmd
29
30
}
30
31
32
+ const vscodeDesktopName = "VS Code Desktop"
33
+
31
34
func (r * RootCmd ) openVSCode () * clibase.Cmd {
32
- var testNoOpen bool
35
+ var (
36
+ generateToken bool
37
+ testNoOpen bool
38
+ )
33
39
34
40
client := new (codersdk.Client )
35
41
cmd := & clibase.Cmd {
36
42
Annotations : workspaceCommand ,
37
43
Use : "vscode <workspace> [<directory in workspace>]" ,
38
- Short : "Open a workspace in Visual Studio Code" ,
44
+ Short : "Open a workspace in Visual Studio Code. " ,
39
45
Middleware : clibase .Chain (
40
- clibase .RequireRangeArgs (1 , - 1 ),
46
+ clibase .RequireRangeArgs (1 , 2 ),
41
47
r .InitClient (client ),
42
48
),
43
49
Handler : func (inv * clibase.Invocation ) error {
44
50
ctx , cancel := context .WithCancel (inv .Context ())
45
51
defer cancel ()
46
52
47
- // Prepare an API key. This is for automagical configuration of
48
- // VS Code, however, we could try to probe VS Code settings to see
49
- // if the current configuration is valid. Future improvement idea.
50
- apiKey , err := client .CreateAPIKey (ctx , codersdk .Me )
51
- if err != nil {
52
- return xerrors .Errorf ("create API key: %w" , err )
53
- }
53
+ // Check if we're inside a workspace, and especially inside _this_
54
+ // workspace so we can perform path resolution/expansion. Generally,
55
+ // we know that if we're inside a workspace, `open` can't be used.
56
+ insideAWorkspace := inv .Environ .Get ("CODER" ) == "true"
57
+ inWorkspaceName := inv .Environ .Get ("CODER_WORKSPACE_NAME" ) + "." + inv .Environ .Get ("CODER_WORKSPACE_AGENT_NAME" )
54
58
55
59
// We need a started workspace to figure out e.g. expanded directory.
56
60
// Pehraps the vscode-coder extension could handle this by accepting
57
61
// default_directory=true, then probing the agent. Then we wouldn't
58
62
// need to wait for the agent to start.
59
- workspaceName := inv .Args [0 ]
63
+ workspaceQuery := inv .Args [0 ]
60
64
autostart := true
61
- workspace , workspaceAgent , err := getWorkspaceAndAgent (ctx , inv , client , autostart , codersdk .Me , workspaceName )
65
+ workspace , workspaceAgent , err := getWorkspaceAndAgent (ctx , inv , client , autostart , codersdk .Me , workspaceQuery )
62
66
if err != nil {
63
67
return xerrors .Errorf ("get workspace and agent: %w" , err )
64
68
}
65
69
66
- // We could optionally add a flag to skip wait, like with SSH.
67
- wait := false
68
- for _ , script := range workspaceAgent .Scripts {
69
- if script .StartBlocksLogin {
70
- wait = true
71
- break
70
+ workspaceName := workspace .Name + "." + workspaceAgent .Name
71
+ insideThisWorkspace := insideAWorkspace && inWorkspaceName == workspaceName
72
+
73
+ if ! insideThisWorkspace {
74
+ // We could optionally add a flag to skip wait, like with SSH.
75
+ wait := false
76
+ for _ , script := range workspaceAgent .Scripts {
77
+ if script .StartBlocksLogin {
78
+ wait = true
79
+ break
80
+ }
72
81
}
73
- }
74
- err = cliui .Agent (ctx , inv .Stderr , workspaceAgent .ID , cliui.AgentOptions {
75
- Fetch : client .WorkspaceAgent ,
76
- FetchLogs : client .WorkspaceAgentLogsAfter ,
77
- Wait : wait ,
78
- })
79
- if err != nil {
80
- if xerrors .Is (err , context .Canceled ) {
81
- return cliui .Canceled
82
+ err = cliui .Agent (ctx , inv .Stderr , workspaceAgent .ID , cliui.AgentOptions {
83
+ Fetch : client .WorkspaceAgent ,
84
+ FetchLogs : client .WorkspaceAgentLogsAfter ,
85
+ Wait : wait ,
86
+ })
87
+ if err != nil {
88
+ if xerrors .Is (err , context .Canceled ) {
89
+ return cliui .Canceled
90
+ }
91
+ return xerrors .Errorf ("agent: %w" , err )
82
92
}
83
- return xerrors .Errorf ("agent: %w" , err )
84
- }
85
93
86
- // If the ExpandedDirectory was initially missing, it could mean
87
- // that the agent hadn't reported it in yet. Retry once.
88
- if workspaceAgent .ExpandedDirectory == "" {
89
- autostart = false // Don't retry autostart.
90
- workspace , workspaceAgent , err = getWorkspaceAndAgent (ctx , inv , client , autostart , codersdk .Me , workspaceName )
91
- if err != nil {
92
- return xerrors .Errorf ("get workspace and agent retry: %w" , err )
94
+ // If the ExpandedDirectory was initially missing, it could mean
95
+ // that the agent hadn't reported it in yet. Retry once.
96
+ if workspaceAgent .ExpandedDirectory == "" {
97
+ autostart = false // Don't retry autostart.
98
+ workspace , workspaceAgent , err = getWorkspaceAndAgent (ctx , inv , client , autostart , codersdk .Me , workspaceName )
99
+ if err != nil {
100
+ return xerrors .Errorf ("get workspace and agent retry: %w" , err )
101
+ }
93
102
}
94
103
}
95
104
96
- var folder string
105
+ var directory string
97
106
switch {
98
107
case len (inv .Args ) > 1 :
99
- folder = inv .Args [1 ]
108
+ directory = inv .Args [1 ]
100
109
// Perhaps we could SSH in to expand the directory?
101
- if strings .HasPrefix (folder , "~" ) {
102
- return xerrors .Errorf ("folder path %q not supported, use an absolute path instead" , folder )
110
+ if ! insideThisWorkspace && strings .HasPrefix (directory , "~" ) {
111
+ return xerrors .Errorf ("directory path %q not supported, use an absolute path instead" , directory )
112
+ }
113
+ if insideThisWorkspace {
114
+ directory , err = filepath .Abs (directory )
115
+ if err != nil {
116
+ return xerrors .Errorf ("expand directory: %w" , err )
117
+ }
103
118
}
104
119
case workspaceAgent .ExpandedDirectory != "" :
105
- folder = workspaceAgent .ExpandedDirectory
120
+ directory = workspaceAgent .ExpandedDirectory
121
+ }
122
+
123
+ u , err := url .Parse ("vscode://coder.coder-remote/open" )
124
+ if err != nil {
125
+ return xerrors .Errorf ("parse vscode URI: %w" , err )
106
126
}
107
127
108
128
qp := url.Values {}
109
129
110
130
qp .Add ("url" , client .URL .String ())
111
- qp .Add ("token" , apiKey .Key )
112
131
qp .Add ("owner" , workspace .OwnerName )
113
132
qp .Add ("workspace" , workspace .Name )
114
133
qp .Add ("agent" , workspaceAgent .Name )
115
- if folder != "" {
116
- qp .Add ("folder" , folder )
134
+ if directory != "" {
135
+ qp .Add ("folder" , directory )
117
136
}
118
137
119
- uri := fmt .Sprintf ("vscode://coder.coder-remote/open?%s" , qp .Encode ())
120
- _ , _ = fmt .Fprintf (inv .Stdout , "Opening %s\n " , strings .ReplaceAll (uri , apiKey .Key , "<REDACTED>" ))
138
+ // We always set the token if we believe we can open without
139
+ // printing the URI, otherwise the token must be explicitly
140
+ // requested as it will be printed in plain text.
141
+ if ! insideAWorkspace || generateToken {
142
+ // Prepare an API key. This is for automagical configuration of
143
+ // VS Code, however, if running on a local machine we could try
144
+ // to probe VS Code settings to see if the current configuration
145
+ // is valid. Future improvement idea.
146
+ apiKey , err := client .CreateAPIKey (ctx , codersdk .Me )
147
+ if err != nil {
148
+ return xerrors .Errorf ("create API key: %w" , err )
149
+ }
150
+ qp .Add ("token" , apiKey .Key )
151
+ }
152
+
153
+ u .RawQuery = qp .Encode ()
154
+
155
+ openingPath := workspaceName
156
+ if directory != "" {
157
+ openingPath += ":" + directory
158
+ }
121
159
122
- if testNoOpen {
160
+ if insideAWorkspace {
161
+ _ , _ = fmt .Fprintf (inv .Stderr , "Opening %s in %s is not supported inside a workspace, please open the following URI on your local machine instead:\n \n " , openingPath , vscodeDesktopName )
162
+ _ , _ = fmt .Fprintf (inv .Stdout , "%s\n " , u .String ())
123
163
return nil
164
+ } else {
165
+ _ , _ = fmt .Fprintf (inv .Stderr , "Opening %s in %s\n " , openingPath , vscodeDesktopName )
124
166
}
125
167
126
- err = open .Run (uri )
168
+ if ! testNoOpen {
169
+ err = open .Run (u .String ())
170
+ } else {
171
+ err = xerrors .New ("test.no-open" )
172
+ }
127
173
if err != nil {
128
- return xerrors .Errorf ("open: %w" , err )
174
+ if ! generateToken {
175
+ qp .Del ("token" )
176
+ u .RawQuery = qp .Encode ()
177
+ }
178
+
179
+ _ , _ = fmt .Fprintf (inv .Stderr , "Could not automatically open %s in %s: %s\n " , openingPath , vscodeDesktopName , err )
180
+ _ , _ = fmt .Fprintf (inv .Stderr , "Please open the following URI instead:\n \n " )
181
+ _ , _ = fmt .Fprintf (inv .Stdout , "%s\n " , u .String ())
182
+ return nil
129
183
}
130
184
131
185
return nil
132
186
},
133
187
}
134
188
135
189
cmd .Options = clibase.OptionSet {
190
+ {
191
+ Flag : "generate-token" ,
192
+ Env : "CODER_OPEN_VSCODE_GENERATE_TOKEN" ,
193
+ Description : fmt .Sprintf (
194
+ "Generate an auth token and include it in the vscode:// URI. This is for automagical configuration of %s and not needed if already configured. " +
195
+ "This flag does not need to be specified when running this command on a local machine unless automatic open fails." ,
196
+ vscodeDesktopName ,
197
+ ),
198
+ Value : clibase .BoolOf (& generateToken ),
199
+ },
136
200
{
137
201
Flag : "test.no-open" ,
138
202
Description : "Don't run the open command." ,
0 commit comments