1
- import * as cp from "child_process"
2
1
import { promises as fs } from "fs"
3
2
import * as path from "path"
4
- import { promisify } from "util"
5
3
import * as vscode from "vscode"
6
- import * as nodeWhich from "which"
7
4
import { requestResponse } from "./request"
8
- import { context , debug , extractTar , extractZip , getAssetUrl , onLine , outputChannel , wrapExit } from "./utils"
5
+ import { debug , extractTar , extractZip , getAssetUrl } from "./utils"
9
6
10
7
/**
11
- * Options for installing and authenticating the Coder CLI .
8
+ * Inner function for `download` so it can wrap with a singleton promise .
12
9
*/
13
- export interface CoderOptions {
14
- accessUri ?: string
15
- token ?: string
16
- version ?: string
17
- }
18
-
19
- /**
20
- * Return "true" if the binary is found in $PATH.
21
- */
22
- export const binaryExists = async ( bin : string ) : Promise < boolean > => {
23
- return new Promise ( ( res ) => {
24
- nodeWhich ( bin , ( err ) => res ( ! err ) )
25
- } )
26
- }
27
-
28
- /**
29
- * Run a command with the Coder CLI after making sure it is installed and
30
- * authenticated. On success stdout is returned. On failure the error will
31
- * include stderr in the message.
32
- */
33
- export const execCoder = async ( command : string , opts ?: CoderOptions ) : Promise < string > => {
34
- debug ( `Run command: ${ command } ` )
35
- const coderBinary = await preflight ( opts ?. version )
36
- const output = await promisify ( cp . exec ) ( coderBinary + " " + command )
37
- return output . stdout
38
- }
39
-
40
- /**
41
- * How to invoke the Coder CLI.
42
- *
43
- * The CODER_BINARY environment variable is meant for tests.
44
- */
45
- const coderInvocation = ( ) : { cmd : string ; args : string [ ] } => {
46
- if ( process . env . CODER_BINARY ) {
47
- return JSON . parse ( process . env . CODER_BINARY )
48
- }
49
- return { cmd : process . platform === "win32" ? "coder.exe" : "coder" , args : [ ] }
50
- }
51
-
52
- /**
53
- * Download the Coder CLI to the provided location and return that location.
54
- */
55
- export const download = async ( version : string , downloadPath : string ) : Promise < string > => {
56
- const assetUrl = getAssetUrl ( version )
57
- const response = await requestResponse ( assetUrl )
58
-
59
- await ( assetUrl . endsWith ( ".tar.gz" )
60
- ? extractTar ( response , path . dirname ( downloadPath ) )
61
- : extractZip ( response , path . dirname ( downloadPath ) ) )
62
-
63
- return downloadPath
64
- }
65
-
66
- /**
67
- * Download the Coder CLI if necessary to a temporary location and return that
68
- * location. If it has already been downloaded it will be reused without regard
69
- * to its version (it can be updated to match later).
70
- */
71
- export const maybeDownload = async ( version = "latest" ) : Promise < string > => {
72
- const invocation = coderInvocation ( )
73
- if ( await binaryExists ( invocation . cmd ) ) {
74
- debug ( ` - Found "${ invocation . cmd } " on PATH` )
75
- return [ invocation . cmd , ...invocation . args ] . join ( " " )
76
- }
77
-
10
+ const doDownload = async ( version : string , downloadPath : string ) : Promise < string > => {
78
11
// See if we already downloaded it.
79
- const downloadPath = path . join ( await context ( ) . globalStoragePath , invocation . cmd )
80
12
try {
81
13
await fs . access ( downloadPath )
82
- debug ( ` - Using previously downloaded " ${ invocation . cmd } " ` )
14
+ debug ( ` - Using previously downloaded: ${ downloadPath } ` )
83
15
return downloadPath
84
16
// eslint-disable-next-line @typescript-eslint/no-explicit-any
85
17
} catch ( error : any ) {
@@ -88,125 +20,44 @@ export const maybeDownload = async (version = "latest"): Promise<string> => {
88
20
}
89
21
}
90
22
91
- debug ( ` - Downloading " ${ invocation . cmd } " ${ version } ` )
23
+ debug ( ` - Downloading ${ version } to ${ downloadPath } ` )
92
24
return vscode . window . withProgress (
93
25
{
94
26
location : vscode . ProgressLocation . Notification ,
95
27
title : `Installing Coder CLI ${ version } ` ,
96
28
} ,
97
- ( ) => download ( version , downloadPath ) ,
98
- )
99
- }
100
-
101
- /**
102
- * Download then copy the Coder CLI to the specified location.
103
- */
104
- export const downloadAndInstall = async ( version : string , destination : string ) : Promise < void > => {
105
- const source = await maybeDownload ( version )
106
- await fs . mkdir ( destination , { recursive : true } )
107
- await fs . rename ( source , path . join ( destination , "coder" ) )
108
- }
109
-
110
- /**
111
- * Install the Coder CLI using the provided command.
112
- */
113
- export const install = async ( cmd : string , args : string [ ] ) : Promise < void > => {
114
- outputChannel . show ( )
115
- outputChannel . appendLine ( cmd + " " + args . join ( " " ) )
116
-
117
- const proc = cp . spawn ( cmd , args )
118
- onLine ( proc . stdout , outputChannel . appendLine . bind ( outputChannel ) )
119
- onLine ( proc . stderr , outputChannel . appendLine . bind ( outputChannel ) )
120
-
121
- try {
122
- await wrapExit ( proc )
123
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
124
- } catch ( error : any ) {
125
- outputChannel . appendLine ( error . message )
126
- throw error
127
- }
128
- }
129
-
130
- /**
131
- * Ask the user whether to install the Coder CLI if not already installed.
132
- *
133
- * Return the invocation for the binary for use with `cp.exec()`.
134
- *
135
- * @TODO Currently unused. Should call after connecting to the workspace
136
- * although it might be fine to just keep using the downloaded version?
137
- */
138
- export const maybeInstall = async ( version : string ) : Promise < string > => {
139
- const invocation = coderInvocation ( )
140
- if ( await binaryExists ( invocation . cmd ) ) {
141
- return [ invocation . cmd , ...invocation . args ] . join ( " " )
142
- }
143
-
144
- const actions : string [ ] = [ ]
145
-
146
- // TODO: This will require sudo or we will need to install to a writable
147
- // location and ask the user to add it to their PATH if they have not already.
148
- const destination = "/usr/local/bin"
149
- // actions.push(`Install to ${destination}`)
150
-
151
- if ( await binaryExists ( "brew" ) ) {
152
- actions . push ( "Install with `brew`" )
153
- }
154
-
155
- if ( actions . length === 0 ) {
156
- throw new Error ( `"${ invocation . cmd } " not found in $PATH.` )
157
- }
29
+ async ( ) => {
30
+ const assetUrl = getAssetUrl ( version )
31
+ const response = await requestResponse ( assetUrl )
158
32
159
- const action = await vscode . window . showInformationMessage ( `"${ invocation . cmd } " was not found in $PATH.` , ...actions )
160
- if ( ! action ) {
161
- throw new Error ( `"${ invocation . cmd } " not found in $PATH.` )
162
- }
33
+ await ( assetUrl . endsWith ( ".tar.gz" )
34
+ ? extractTar ( response , path . dirname ( downloadPath ) )
35
+ : extractZip ( response , path . dirname ( downloadPath ) ) )
163
36
164
- await vscode . window . withProgress (
165
- {
166
- location : vscode . ProgressLocation . Notification ,
167
- title : `Installing Coder CLI` ,
168
- } ,
169
- async ( ) => {
170
- switch ( action ) {
171
- case `Install to ${ destination } ` :
172
- return downloadAndInstall ( version , destination )
173
- case "Install with `brew`" :
174
- return install ( "brew" , [ "install" , "cdr/coder/coder-cli@${version}" ] )
175
- }
37
+ return downloadPath
176
38
} ,
177
39
)
178
-
179
- // See if we can now find it via the path.
180
- if ( await binaryExists ( invocation . cmd ) ) {
181
- return [ invocation . cmd , ...invocation . args ] . join ( " " )
182
- } else {
183
- throw new Error ( `"${ invocation . cmd } " still not found in $PATH.` )
184
- }
185
40
}
186
41
187
- /** Only one preflight request at a time. */
188
- let _preflight : Promise < string > | undefined
42
+ /** Only one request at a time. */
43
+ let promise : Promise < string > | undefined
189
44
190
45
/**
191
- * Check that Coder is installed. If not try installing.
192
- *
193
- * Return the appropriate invocation for the binary.
46
+ * Download the Coder CLI if necessary to the provided location while showing a
47
+ * progress bar then return that location. If it has already been downloaded it
48
+ * will be reused without regard to its version (it can be updated to match
49
+ * later). This function is safe to call multiple times concurrently.
194
50
*/
195
- export const preflight = async ( version = "latest" ) : Promise < string > => {
196
- if ( ! _preflight ) {
197
- _preflight = ( async ( ) : Promise < string > => {
51
+ export const download = async ( version : string , downloadPath : string ) : Promise < string > => {
52
+ if ( ! promise ) {
53
+ promise = ( async ( ) : Promise < string > => {
198
54
try {
199
- return await maybeDownload ( version )
200
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
201
- } catch ( error : any ) {
202
- throw new Error ( `${ error . message } . Please [install manually](https://coder.com/docs/cli/installation).` )
55
+ return await doDownload ( version , downloadPath )
203
56
} finally {
204
- // Clear after completion so we can try again in the case of errors, if
205
- // the binary is removed, etc.
206
- _preflight = undefined
57
+ promise = undefined
207
58
}
208
59
} ) ( )
209
60
}
210
61
211
- return _preflight
62
+ return promise
212
63
}
0 commit comments