1
+ const os = require ( 'os' ) ;
2
+ const path = require ( 'path' ) ;
3
+ const fs = require ( 'fs-extra' ) ;
4
+ const pkg = require ( '../../package' ) ;
5
+ const semver = require ( 'semver' ) ;
1
6
const log = require ( '../lib/log' ) ;
2
7
const chalk = require ( 'chalk' ) ;
8
+ const settings = require ( '../../settings' ) ;
9
+ const request = require ( 'request' ) ;
10
+ const zlib = require ( 'zlib' ) ;
11
+ const Spinner = require ( 'cli-spinner' ) . Spinner ;
12
+ const crypto = require ( 'crypto' ) ;
3
13
4
14
/*
5
15
* The update-cli command tells the CLI installer to reinstall the latest version of the CLI
@@ -8,16 +18,186 @@ const chalk = require('chalk');
8
18
* If the CLI was installed using npm, tell the user to update using npm
9
19
*/
10
20
class UpdateCliCommand {
11
- update ( { 'enable-updates' : enableUpdates , 'disable-updates' : disableUpdates } ) {
21
+ update ( { 'enable-updates' : enableUpdates , 'disable-updates' : disableUpdates , version } ) {
12
22
if ( enableUpdates ) {
13
- log . info ( 'Automatic update checks are now enabled' ) ;
14
- return ;
23
+ return this . enableUpdates ( ) ;
15
24
}
16
25
if ( disableUpdates ) {
17
- log . info ( 'Automatic update checks are now disabled' ) ;
26
+ return this . disableUpdates ( ) ;
27
+ }
28
+ if ( ! process . pkg ) {
29
+ log . info ( `Update the CLI by running ${ chalk . bold ( 'npm install -g particle-cli' ) } ` ) ;
30
+ log . info ( 'To stay up to date with the latest features and improvements, please install the latest Particle Installer executable from our website: https://www.particle.io/cli' ) ;
31
+ return ;
32
+ }
33
+ return this . updateCli ( version ) ;
34
+ }
35
+
36
+ async enableUpdates ( ) {
37
+ // set the update flag to true
38
+ settings . profile_json . enableUpdates = true ;
39
+ settings . saveProfileData ( ) ;
40
+ log . info ( 'Automatic update checks are now enabled' ) ;
41
+ }
42
+ async disableUpdates ( ) {
43
+ // set the update flag to false
44
+ settings . profile_json . enableUpdates = false ;
45
+ settings . saveProfileData ( ) ;
46
+ log . info ( 'Automatic update checks are now disabled' ) ;
47
+ }
48
+
49
+ async updateCli ( version ) {
50
+ log . info ( `Updating the CLI to ${ version ? version : 'latest' } ` ) ;
51
+ const spinner = new Spinner ( 'Updating CLI...' ) ;
52
+ spinner . start ( ) ;
53
+ // download manifest
54
+ const manifest = await this . downloadManifest ( version ) ;
55
+ const upToDate = semver . gte ( pkg . version , manifest . version ) && ! version ;
56
+ if ( upToDate ) {
57
+ spinner . stop ( true ) ;
58
+ log . info ( 'CLI is already up to date' ) ;
18
59
return ;
19
60
}
20
- log . info ( `Update the CLI by running ${ chalk . bold ( 'npm install -g particle-cli' ) } ` ) ;
61
+ const cliPath = await this . downloadCLI ( manifest ) ;
62
+ await this . replaceCLI ( cliPath ) ;
63
+ spinner . stop ( true ) ;
64
+ await this . configureProfileSettings ( version ) ;
65
+ log . info ( 'CLI updated successfully' ) ;
66
+ }
67
+
68
+ async downloadManifest ( version ) {
69
+ const fileName = version ? `manifest-${ version } .json` : 'manifest.json' ;
70
+ const url = `https://${ settings . manifestHost } /particle-cli/${ fileName } ` ;
71
+ return new Promise ( ( resolve , reject ) => {
72
+ return request ( url , ( error , response , body ) => {
73
+ if ( error ) {
74
+ return this . logAndReject ( error , reject , version ) ;
75
+ }
76
+ if ( response . statusCode !== 200 ) {
77
+ return this . logAndReject ( `Failed to download manifest: Status Code ${ response . statusCode } ` , reject , version ) ;
78
+ }
79
+ try {
80
+ resolve ( JSON . parse ( body ) ) ;
81
+ } catch ( error ) {
82
+ this . logAndReject ( error , reject , version ) ;
83
+ }
84
+ } ) ;
85
+ } ) ;
86
+ }
87
+
88
+ logAndReject ( error , reject , version ) {
89
+ const baseMessage = 'We were unable to check for updates' ;
90
+ const message = version ? `${ baseMessage } : Version ${ version } not found` : `${ baseMessage } Please try again later` ;
91
+ log . error ( error ) ;
92
+ reject ( message ) ;
93
+ }
94
+
95
+ async downloadCLI ( manifest ) {
96
+ try {
97
+ const { url, sha256 : expectedHash } = this . getBuildDetailsFromManifest ( manifest ) ;
98
+ const fileName = url . split ( '/' ) . pop ( ) ;
99
+ const fileNameWithoutLastExtension = path . basename ( fileName , path . extname ( fileName ) ) ;
100
+ const filePath = path . join ( os . tmpdir ( ) , fileNameWithoutLastExtension ) ;
101
+ const tempFilePath = `${ filePath } .gz` ;
102
+
103
+ const output = fs . createWriteStream ( tempFilePath ) ;
104
+
105
+ return await new Promise ( ( resolve , reject ) => {
106
+ request ( url )
107
+ . on ( 'response' , ( response ) => {
108
+ if ( response . statusCode !== 200 ) {
109
+ log . debug ( `Failed to download CLI: Status Code ${ response . statusCode } ` ) ;
110
+ return reject ( new Error ( 'No file found to download' ) ) ;
111
+ }
112
+ } )
113
+ . pipe ( output )
114
+ . on ( 'finish' , async ( ) => {
115
+ const fileHash = await this . getFileHash ( tempFilePath ) ;
116
+ if ( fileHash === expectedHash ) {
117
+ const unzipPath = await this . unzipFile ( tempFilePath , filePath ) ;
118
+ resolve ( unzipPath ) ;
119
+ } else {
120
+ reject ( new Error ( 'Hash mismatch' ) ) ;
121
+ }
122
+ } )
123
+ . on ( 'error' , ( error ) => {
124
+ reject ( error ) ;
125
+ } ) ;
126
+ } ) ;
127
+ } catch ( error ) {
128
+ log . debug ( `Failed during download or verification: ${ error } ` ) ;
129
+ throw new Error ( 'Failed to download or verify the CLI, please try again later' ) ;
130
+ }
131
+ }
132
+
133
+ async getFileHash ( filePath ) {
134
+ return new Promise ( ( resolve , reject ) => {
135
+ const hash = crypto . createHash ( 'sha256' ) ;
136
+ const stream = fs . createReadStream ( filePath ) ;
137
+ stream . on ( 'data' , ( data ) => hash . update ( data ) ) ;
138
+ stream . on ( 'end' , ( ) => resolve ( hash . digest ( 'hex' ) ) ) ;
139
+ stream . on ( 'error' , ( error ) => reject ( error ) ) ;
140
+ } ) ;
141
+ }
142
+
143
+ async unzipFile ( sourcePath , targetPath ) {
144
+ return new Promise ( ( resolve , reject ) => {
145
+ const gunzip = zlib . createGunzip ( ) ;
146
+ const source = fs . createReadStream ( sourcePath ) ;
147
+ const destination = fs . createWriteStream ( targetPath ) ;
148
+ source
149
+ . pipe ( gunzip )
150
+ . pipe ( destination )
151
+ . on ( 'finish' , ( ) => resolve ( targetPath ) )
152
+ . on ( 'error' , ( error ) => reject ( error ) ) ;
153
+ } ) ;
154
+ }
155
+
156
+ getBuildDetailsFromManifest ( manifest ) {
157
+ const platformMapping = {
158
+ darwin : 'darwin' ,
159
+ linux : 'linux' ,
160
+ win32 : 'win'
161
+ } ;
162
+ const archMapping = {
163
+ x64 : 'amd64' ,
164
+ arm64 : 'arm64'
165
+ } ;
166
+ const platform = os . platform ( ) ;
167
+ const arch = os . arch ( ) ;
168
+ const platformKey = platformMapping [ platform ] || platform ;
169
+ const archKey = archMapping [ arch ] || arch ;
170
+ const platformManifest = manifest . builds && manifest . builds [ platformKey ] ;
171
+ const archManifest = platformManifest && platformManifest [ archKey ] ;
172
+ if ( ! archManifest ) {
173
+ throw new Error ( `No CLI build found for ${ platform } ${ arch } ` ) ;
174
+ }
175
+ return archManifest ;
176
+ }
177
+
178
+ async replaceCLI ( newCliPath ) {
179
+ // rename the original CLI
180
+ const binPath = this . getBinaryPath ( ) ;
181
+ const fileName = os . platform ( ) === 'win32' ? 'particle.exe' : 'particle' ;
182
+ const cliPath = path . join ( binPath , fileName ) ;
183
+ const oldCliPath = path . join ( binPath , `${ fileName } .old` ) ;
184
+ await fs . move ( cliPath , oldCliPath , { overwrite : true } ) ;
185
+ await fs . move ( newCliPath , cliPath ) ;
186
+ await fs . chmod ( cliPath , 0o755 ) ; // add execute permissions
187
+ }
188
+
189
+ getBinaryPath ( ) {
190
+ if ( os . platform ( ) === 'win32' ) {
191
+ return path . join ( process . env . LOCALAPPDATA , 'particle' , 'bin' ) ;
192
+ }
193
+ return path . join ( os . homedir ( ) , 'bin' ) ;
194
+ }
195
+ async configureProfileSettings ( version ) {
196
+ settings . profile_json . last_version_check = new Date ( ) . getTime ( ) ;
197
+ settings . saveProfileData ( ) ;
198
+ if ( version ) {
199
+ await this . disableUpdates ( ) ; // disable updates since we are installing a specific version
200
+ }
21
201
}
22
202
}
23
203
0 commit comments