11// CLI Installer — downloads the correct Napper CLI binary from GitHub releases
22// Decoupled from vscode SDK — takes config values as parameters
33
4+ import type * as http from "http" ;
45import * as https from "https" ;
56import * as fs from "fs" ;
67import * as path from "path" ;
@@ -29,19 +30,27 @@ import {
2930 CLI_FILE_MODE_EXECUTABLE ,
3031} from "./constants" ;
3132
33+ const HTTP_STATUS_OK = 200 ;
34+ const HTTP_STATUS_REDIRECT_MIN = 300 ;
35+ const HTTP_STATUS_CLIENT_ERROR_MIN = 400 ;
36+
37+ const PLATFORM_RID_MAP : ReadonlyMap < string , string > = new Map ( [
38+ [ `${ CLI_PLATFORM_DARWIN } -${ CLI_ARCH_ARM64 } ` , CLI_RID_OSX_ARM64 ] ,
39+ [ `${ CLI_PLATFORM_DARWIN } -${ CLI_ARCH_X64 } ` , CLI_RID_OSX_X64 ] ,
40+ [ `${ CLI_PLATFORM_LINUX } -${ CLI_ARCH_X64 } ` , CLI_RID_LINUX_X64 ] ,
41+ [ `${ CLI_PLATFORM_WIN32 } -${ CLI_ARCH_X64 } ` , CLI_RID_WIN_X64 ] ,
42+ ] ) ;
43+
3244export const platformToRid = (
3345 platform : string ,
3446 arch : string
3547) : Result < string , string > => {
36- if ( platform === CLI_PLATFORM_DARWIN && arch === CLI_ARCH_ARM64 )
37- return ok ( CLI_RID_OSX_ARM64 ) ;
38- if ( platform === CLI_PLATFORM_DARWIN && arch === CLI_ARCH_X64 )
39- return ok ( CLI_RID_OSX_X64 ) ;
40- if ( platform === CLI_PLATFORM_LINUX && arch === CLI_ARCH_X64 )
41- return ok ( CLI_RID_LINUX_X64 ) ;
42- if ( platform === CLI_PLATFORM_WIN32 && arch === CLI_ARCH_X64 )
43- return ok ( CLI_RID_WIN_X64 ) ;
44- return err ( `${ CLI_UNSUPPORTED_PLATFORM_MSG } ${ platform } -${ arch } ` ) ;
48+ const key = `${ platform } -${ arch } ` ;
49+ const rid = PLATFORM_RID_MAP . get ( key ) ;
50+ if ( rid !== undefined ) {
51+ return ok ( rid ) ;
52+ }
53+ return err ( `${ CLI_UNSUPPORTED_PLATFORM_MSG } ${ key } ` ) ;
4554} ;
4655
4756export const assetName = ( rid : string ) : string => {
@@ -62,57 +71,88 @@ export const installedCliPath = (
6271export const isCliInstalled = ( cliPath : string ) : boolean =>
6372 fs . existsSync ( cliPath ) ;
6473
65- const followRedirect = (
74+ interface RedirectContext {
75+ readonly dest : string ;
76+ readonly redirectCount : number ;
77+ readonly resolve : ( value : Result < void , string > ) => void ;
78+ }
79+
80+ const handleRedirect = (
81+ response : http . IncomingMessage ,
82+ ctx : RedirectContext ,
83+ ) : void => {
84+ const location = response . headers . location ;
85+ if ( location === undefined || location === "" ) {
86+ ctx . resolve ( err ( CLI_REDIRECT_ERROR ) ) ;
87+ return ;
88+ }
89+ response . resume ( ) ;
90+ followRedirect ( location , ctx . dest , ctx . redirectCount + 1 )
91+ . then ( ctx . resolve )
92+ . catch ( ( ) => { ctx . resolve ( err ( CLI_REDIRECT_ERROR ) ) ; } ) ;
93+ } ;
94+
95+ const handleDownload = (
96+ response : http . IncomingMessage ,
97+ dest : string ,
98+ resolve : ( value : Result < void , string > ) => void
99+ ) : void => {
100+ const file = fs . createWriteStream ( dest ) ;
101+ response . pipe ( file ) ;
102+ file . on ( "finish" , ( ) => {
103+ file . close ( ) ;
104+ resolve ( ok ( undefined ) ) ;
105+ } ) ;
106+ file . on ( "error" , ( e ) => { resolve ( err ( e . message ) ) ; } ) ;
107+ } ;
108+
109+ const buildRequestOptions = ( url : string ) : { hostname : string ; path : string ; headers : Record < string , string > } => {
110+ const parsedUrl = new URL ( url ) ;
111+ return {
112+ hostname : parsedUrl . hostname ,
113+ path : parsedUrl . pathname + parsedUrl . search ,
114+ headers : { "User-Agent" : CLI_BINARY_NAME } ,
115+ } ;
116+ } ;
117+
118+ const isRedirectStatus = ( status : number ) : boolean =>
119+ status >= HTTP_STATUS_REDIRECT_MIN && status < HTTP_STATUS_CLIENT_ERROR_MIN ;
120+
121+ const handleResponse = (
122+ response : http . IncomingMessage ,
123+ ctx : RedirectContext ,
124+ ) : void => {
125+ const status = response . statusCode ?? 0 ;
126+ if ( isRedirectStatus ( status ) ) {
127+ handleRedirect ( response , ctx ) ;
128+ } else if ( status !== HTTP_STATUS_OK ) {
129+ response . resume ( ) ;
130+ ctx . resolve ( err ( `${ CLI_DOWNLOAD_ERROR_PREFIX } ${ status } ` ) ) ;
131+ } else {
132+ handleDownload ( response , ctx . dest , ctx . resolve ) ;
133+ }
134+ } ;
135+
136+ async function followRedirect (
66137 url : string ,
67138 dest : string ,
68139 redirectCount : number
69- ) : Promise < Result < void , string > > => {
140+ ) : Promise < Result < void , string > > {
70141 if ( redirectCount > CLI_MAX_REDIRECTS ) {
71- return Promise . resolve ( err ( CLI_TOO_MANY_REDIRECTS ) ) ;
142+ return err ( CLI_TOO_MANY_REDIRECTS ) ;
72143 }
73144
74- return new Promise ( ( resolve ) => {
75- const parsedUrl = new URL ( url ) ;
76- const options = {
77- hostname : parsedUrl . hostname ,
78- path : parsedUrl . pathname + parsedUrl . search ,
79- headers : { "User-Agent" : CLI_BINARY_NAME } ,
80- } ;
145+ const options = buildRequestOptions ( url ) ;
81146
147+ return await new Promise ( ( resolve ) => {
148+ const ctx : RedirectContext = { dest, redirectCount, resolve } ;
82149 https
83- . get ( options , ( response ) => {
84- const status = response . statusCode ?? 0 ;
85-
86- if ( status >= 300 && status < 400 ) {
87- const location = response . headers . location ;
88- if ( ! location ) {
89- resolve ( err ( CLI_REDIRECT_ERROR ) ) ;
90- return ;
91- }
92- response . resume ( ) ;
93- resolve ( followRedirect ( location , dest , redirectCount + 1 ) ) ;
94- return ;
95- }
96-
97- if ( status !== 200 ) {
98- response . resume ( ) ;
99- resolve ( err ( `${ CLI_DOWNLOAD_ERROR_PREFIX } ${ status } ` ) ) ;
100- return ;
101- }
102-
103- const file = fs . createWriteStream ( dest ) ;
104- response . pipe ( file ) ;
105- file . on ( "finish" , ( ) => {
106- file . close ( ) ;
107- resolve ( ok ( undefined ) ) ;
108- } ) ;
109- file . on ( "error" , ( e ) => resolve ( err ( e . message ) ) ) ;
110- } )
111- . on ( "error" , ( e ) => resolve ( err ( e . message ) ) ) ;
150+ . get ( options , ( response ) => { handleResponse ( response , ctx ) ; } )
151+ . on ( "error" , ( e ) => { resolve ( err ( e . message ) ) ; } ) ;
112152 } ) ;
113- } ;
153+ }
114154
115- export const downloadBinary = (
155+ export const downloadBinary = async (
116156 rid : string ,
117157 destPath : string
118158) : Promise < Result < void , string > > => {
@@ -124,7 +164,7 @@ export const downloadBinary = (
124164 fs . mkdirSync ( dir , { recursive : true } ) ;
125165 }
126166
127- return followRedirect ( url , destPath , 0 ) ;
167+ return await followRedirect ( url , destPath , 0 ) ;
128168} ;
129169
130170export const makeExecutable = (
@@ -146,11 +186,15 @@ export const installCli = async (
146186 arch : string
147187) : Promise < Result < InstallResult , string > > => {
148188 const ridResult = platformToRid ( platform , arch ) ;
149- if ( ! ridResult . ok ) return err ( ridResult . error ) ;
189+ if ( ! ridResult . ok ) {
190+ return err ( ridResult . error ) ;
191+ }
150192
151193 const destPath = installedCliPath ( storageDir , platform ) ;
152194 const downloadResult = await downloadBinary ( ridResult . value , destPath ) ;
153- if ( ! downloadResult . ok ) return err ( downloadResult . error ) ;
195+ if ( ! downloadResult . ok ) {
196+ return err ( downloadResult . error ) ;
197+ }
154198
155199 makeExecutable ( destPath , platform ) ;
156200 return ok ( { cliPath : destPath } ) ;
0 commit comments