@oclif/plugin-update 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/lib/commands/update.d.ts +0 -20
- package/lib/commands/update.js +6 -334
- package/lib/update.d.ts +36 -0
- package/lib/update.js +331 -0
- package/oclif.manifest.json +1 -1
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,13 @@
|
|
2
2
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
4
4
|
|
5
|
+
## [2.1.0](https://github.com/oclif/plugin-update/compare/v2.0.0...v2.1.0) (2021-11-23)
|
6
|
+
|
7
|
+
|
8
|
+
### Features
|
9
|
+
|
10
|
+
* remove legacy update logic ([#314](https://github.com/oclif/plugin-update/issues/314)) ([5241c20](https://github.com/oclif/plugin-update/commit/5241c20dc4eea215bfdedfccecc2e4acf3d3465b))
|
11
|
+
|
5
12
|
## [1.5.0](https://github.com/oclif/plugin-update/compare/v1.4.0...v1.5.0) (2021-08-05)
|
6
13
|
|
7
14
|
## [1.4.0](https://github.com/oclif/plugin-update/compare/v1.4.0-3...v1.4.0) (2021-08-05)
|
package/lib/commands/update.d.ts
CHANGED
@@ -9,28 +9,8 @@ export default class UpdateCommand extends Command {
|
|
9
9
|
autoupdate: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
10
10
|
'from-local': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
11
11
|
};
|
12
|
-
private autoupdate;
|
13
12
|
private channel;
|
14
|
-
private currentVersion?;
|
15
|
-
private updatedVersion;
|
16
13
|
private readonly clientRoot;
|
17
14
|
private readonly clientBin;
|
18
15
|
run(): Promise<void>;
|
19
|
-
private fetchManifest;
|
20
|
-
private downloadAndExtract;
|
21
|
-
private update;
|
22
|
-
private updateToExistingVersion;
|
23
|
-
private skipUpdate;
|
24
|
-
private determineChannel;
|
25
|
-
private determineCurrentVersion;
|
26
|
-
private s3ChannelManifestKey;
|
27
|
-
private setChannel;
|
28
|
-
private logChop;
|
29
|
-
private mtime;
|
30
|
-
private debounce;
|
31
|
-
private tidy;
|
32
|
-
private touch;
|
33
|
-
private reexec;
|
34
|
-
private createBin;
|
35
|
-
private ensureClientDir;
|
36
16
|
}
|
package/lib/commands/update.js
CHANGED
@@ -1,14 +1,12 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
-
const color_1 = require("@oclif/color");
|
4
3
|
const core_1 = require("@oclif/core");
|
5
4
|
const cli_ux_1 = require("cli-ux");
|
6
|
-
const spawn = require("cross-spawn");
|
7
|
-
const fs = require("fs-extra");
|
8
|
-
const _ = require("lodash");
|
9
5
|
const path = require("path");
|
10
|
-
const
|
11
|
-
|
6
|
+
const update_1 = require("../update");
|
7
|
+
async function getPinToVersion() {
|
8
|
+
return cli_ux_1.default.prompt('Enter a version to update to');
|
9
|
+
}
|
12
10
|
class UpdateCommand extends core_1.Command {
|
13
11
|
constructor() {
|
14
12
|
super(...arguments);
|
@@ -17,334 +15,8 @@ class UpdateCommand extends core_1.Command {
|
|
17
15
|
}
|
18
16
|
async run() {
|
19
17
|
const { args, flags } = await this.parse(UpdateCommand);
|
20
|
-
|
21
|
-
|
22
|
-
await this.debounce();
|
23
|
-
this.channel = args.channel || await this.determineChannel();
|
24
|
-
if (flags['from-local']) {
|
25
|
-
await this.ensureClientDir();
|
26
|
-
this.debug(`Looking for locally installed versions at ${this.clientRoot}`);
|
27
|
-
// Do not show known non-local version folder names, bin and current.
|
28
|
-
const versions = fs.readdirSync(this.clientRoot).filter(dirOrFile => dirOrFile !== 'bin' && dirOrFile !== 'current');
|
29
|
-
if (versions.length === 0)
|
30
|
-
throw new Error('No locally installed versions found.');
|
31
|
-
this.log(`Found versions: \n${versions.map(version => ` ${version}`).join('\n')}\n`);
|
32
|
-
const pinToVersion = await cli_ux_1.default.prompt('Enter a version to update to');
|
33
|
-
if (!versions.includes(pinToVersion))
|
34
|
-
throw new Error(`Version ${pinToVersion} not found in the locally installed versions.`);
|
35
|
-
if (!await fs.pathExists(path.join(this.clientRoot, pinToVersion))) {
|
36
|
-
throw new Error(`Version ${pinToVersion} is not already installed at ${this.clientRoot}.`);
|
37
|
-
}
|
38
|
-
cli_ux_1.default.action.start(`${this.config.name}: Updating CLI`);
|
39
|
-
this.debug(`switching to existing version ${pinToVersion}`);
|
40
|
-
this.updateToExistingVersion(pinToVersion);
|
41
|
-
this.log();
|
42
|
-
this.log(`Updating to an already installed version will not update the channel. If autoupdate is enabled, the CLI will eventually be updated back to ${this.channel}.`);
|
43
|
-
}
|
44
|
-
else {
|
45
|
-
cli_ux_1.default.action.start(`${this.config.name}: Updating CLI`);
|
46
|
-
await this.config.runHook('preupdate', { channel: this.channel });
|
47
|
-
const manifest = await this.fetchManifest();
|
48
|
-
this.currentVersion = await this.determineCurrentVersion();
|
49
|
-
this.updatedVersion = manifest.sha ? `${manifest.version}-${manifest.sha}` : manifest.version;
|
50
|
-
const reason = await this.skipUpdate();
|
51
|
-
if (reason)
|
52
|
-
cli_ux_1.default.action.stop(reason || 'done');
|
53
|
-
else
|
54
|
-
await this.update(manifest);
|
55
|
-
this.debug('tidy');
|
56
|
-
await this.tidy();
|
57
|
-
await this.config.runHook('update', { channel: this.channel });
|
58
|
-
}
|
59
|
-
this.debug('done');
|
60
|
-
cli_ux_1.default.action.stop();
|
61
|
-
}
|
62
|
-
async fetchManifest() {
|
63
|
-
const http = require('http-call').HTTP;
|
64
|
-
cli_ux_1.default.action.status = 'fetching manifest';
|
65
|
-
if (!this.config.scopedEnvVarTrue('USE_LEGACY_UPDATE')) {
|
66
|
-
try {
|
67
|
-
const newManifestUrl = this.config.s3Url(this.s3ChannelManifestKey(this.config.bin, this.config.platform, this.config.arch, this.config.pjson.oclif.update.s3.folder));
|
68
|
-
const { body } = await http.get(newManifestUrl);
|
69
|
-
if (typeof body === 'string') {
|
70
|
-
return JSON.parse(body);
|
71
|
-
}
|
72
|
-
return body;
|
73
|
-
}
|
74
|
-
catch (error) {
|
75
|
-
this.debug(error.message);
|
76
|
-
}
|
77
|
-
}
|
78
|
-
try {
|
79
|
-
const url = this.config.s3Url(this.config.s3Key('manifest', {
|
80
|
-
channel: this.channel,
|
81
|
-
platform: this.config.platform,
|
82
|
-
arch: this.config.arch,
|
83
|
-
}));
|
84
|
-
const { body } = await http.get(url);
|
85
|
-
// in case the content-type is not set, parse as a string
|
86
|
-
// this will happen if uploading without `oclif-dev publish`
|
87
|
-
if (typeof body === 'string') {
|
88
|
-
return JSON.parse(body);
|
89
|
-
}
|
90
|
-
return body;
|
91
|
-
}
|
92
|
-
catch (error) {
|
93
|
-
if (error.statusCode === 403)
|
94
|
-
throw new Error(`HTTP 403: Invalid channel ${this.channel}`);
|
95
|
-
throw error;
|
96
|
-
}
|
97
|
-
}
|
98
|
-
async downloadAndExtract(output, manifest, channel) {
|
99
|
-
const { version } = manifest;
|
100
|
-
const filesize = (n) => {
|
101
|
-
const [num, suffix] = require('filesize')(n, { output: 'array' });
|
102
|
-
return num.toFixed(1) + ` ${suffix}`;
|
103
|
-
};
|
104
|
-
const http = require('http-call').HTTP;
|
105
|
-
const gzUrl = manifest.gz || this.config.s3Url(this.config.s3Key('versioned', {
|
106
|
-
version,
|
107
|
-
channel,
|
108
|
-
bin: this.config.bin,
|
109
|
-
platform: this.config.platform,
|
110
|
-
arch: this.config.arch,
|
111
|
-
ext: 'gz',
|
112
|
-
}));
|
113
|
-
const { response: stream } = await http.stream(gzUrl);
|
114
|
-
stream.pause();
|
115
|
-
const baseDir = manifest.baseDir || this.config.s3Key('baseDir', {
|
116
|
-
version,
|
117
|
-
channel,
|
118
|
-
bin: this.config.bin,
|
119
|
-
platform: this.config.platform,
|
120
|
-
arch: this.config.arch,
|
121
|
-
});
|
122
|
-
const extraction = (0, tar_1.extract)(stream, baseDir, output, manifest.sha256gz);
|
123
|
-
// to-do: use cli.action.type
|
124
|
-
if (cli_ux_1.default.action.frames) {
|
125
|
-
// if spinner action
|
126
|
-
const total = parseInt(stream.headers['content-length'], 10);
|
127
|
-
let current = 0;
|
128
|
-
const updateStatus = _.throttle((newStatus) => {
|
129
|
-
cli_ux_1.default.action.status = newStatus;
|
130
|
-
}, 250, { leading: true, trailing: false });
|
131
|
-
stream.on('data', data => {
|
132
|
-
current += data.length;
|
133
|
-
updateStatus(`${filesize(current)}/${filesize(total)}`);
|
134
|
-
});
|
135
|
-
}
|
136
|
-
stream.resume();
|
137
|
-
await extraction;
|
138
|
-
}
|
139
|
-
async update(manifest, channel = 'stable') {
|
140
|
-
const { channel: manifestChannel } = manifest;
|
141
|
-
if (manifestChannel)
|
142
|
-
channel = manifestChannel;
|
143
|
-
cli_ux_1.default.action.start(`${this.config.name}: Updating CLI from ${color_1.default.green(this.currentVersion)} to ${color_1.default.green(this.updatedVersion)}${channel === 'stable' ? '' : ' (' + color_1.default.yellow(channel) + ')'}`);
|
144
|
-
await this.ensureClientDir();
|
145
|
-
const output = path.join(this.clientRoot, this.updatedVersion);
|
146
|
-
if (!await fs.pathExists(output)) {
|
147
|
-
await this.downloadAndExtract(output, manifest, channel);
|
148
|
-
}
|
149
|
-
await this.setChannel();
|
150
|
-
await this.createBin(this.updatedVersion);
|
151
|
-
await this.touch();
|
152
|
-
await this.reexec();
|
153
|
-
}
|
154
|
-
async updateToExistingVersion(version) {
|
155
|
-
await this.createBin(version);
|
156
|
-
await this.touch();
|
157
|
-
}
|
158
|
-
async skipUpdate() {
|
159
|
-
if (!this.config.binPath) {
|
160
|
-
const instructions = this.config.scopedEnvVar('UPDATE_INSTRUCTIONS');
|
161
|
-
if (instructions)
|
162
|
-
this.warn(instructions);
|
163
|
-
return 'not updatable';
|
164
|
-
}
|
165
|
-
if (this.currentVersion === this.updatedVersion) {
|
166
|
-
if (this.config.scopedEnvVar('HIDE_UPDATED_MESSAGE'))
|
167
|
-
return 'done';
|
168
|
-
return `already on latest version: ${this.currentVersion}`;
|
169
|
-
}
|
170
|
-
return false;
|
171
|
-
}
|
172
|
-
async determineChannel() {
|
173
|
-
const channelPath = path.join(this.config.dataDir, 'channel');
|
174
|
-
if (fs.existsSync(channelPath)) {
|
175
|
-
const channel = await fs.readFile(channelPath, 'utf8');
|
176
|
-
return String(channel).trim();
|
177
|
-
}
|
178
|
-
return this.config.channel || 'stable';
|
179
|
-
}
|
180
|
-
async determineCurrentVersion() {
|
181
|
-
try {
|
182
|
-
const currentVersion = await fs.readFile(this.clientBin, 'utf8');
|
183
|
-
const matches = currentVersion.match(/\.\.[/|\\](.+)[/|\\]bin/);
|
184
|
-
return matches ? matches[1] : this.config.version;
|
185
|
-
}
|
186
|
-
catch (error) {
|
187
|
-
this.debug(error);
|
188
|
-
}
|
189
|
-
return this.config.version;
|
190
|
-
}
|
191
|
-
s3ChannelManifestKey(bin, platform, arch, folder) {
|
192
|
-
let s3SubDir = folder || '';
|
193
|
-
if (s3SubDir !== '' && s3SubDir.slice(-1) !== '/')
|
194
|
-
s3SubDir = `${s3SubDir}/`;
|
195
|
-
return path.join(s3SubDir, 'channels', this.channel, `${bin}-${platform}-${arch}-buildmanifest`);
|
196
|
-
}
|
197
|
-
async setChannel() {
|
198
|
-
const channelPath = path.join(this.config.dataDir, 'channel');
|
199
|
-
fs.writeFile(channelPath, this.channel, 'utf8');
|
200
|
-
}
|
201
|
-
async logChop() {
|
202
|
-
try {
|
203
|
-
this.debug('log chop');
|
204
|
-
const logChopper = require('log-chopper').default;
|
205
|
-
await logChopper.chop(this.config.errlog);
|
206
|
-
}
|
207
|
-
catch (error) {
|
208
|
-
this.debug(error.message);
|
209
|
-
}
|
210
|
-
}
|
211
|
-
async mtime(f) {
|
212
|
-
const { mtime } = await fs.stat(f);
|
213
|
-
return mtime;
|
214
|
-
}
|
215
|
-
// when autoupdating, wait until the CLI isn't active
|
216
|
-
async debounce() {
|
217
|
-
let output = false;
|
218
|
-
const lastrunfile = path.join(this.config.cacheDir, 'lastrun');
|
219
|
-
const m = await this.mtime(lastrunfile);
|
220
|
-
m.setHours(m.getHours() + 1);
|
221
|
-
if (m > new Date()) {
|
222
|
-
const msg = `waiting until ${m.toISOString()} to update`;
|
223
|
-
if (output) {
|
224
|
-
this.debug(msg);
|
225
|
-
}
|
226
|
-
else {
|
227
|
-
await cli_ux_1.default.log(msg);
|
228
|
-
output = true;
|
229
|
-
}
|
230
|
-
await (0, util_1.wait)(60 * 1000); // wait 1 minute
|
231
|
-
return this.debounce();
|
232
|
-
}
|
233
|
-
cli_ux_1.default.log('time to update');
|
234
|
-
}
|
235
|
-
// removes any unused CLIs
|
236
|
-
async tidy() {
|
237
|
-
try {
|
238
|
-
const root = this.clientRoot;
|
239
|
-
if (!await fs.pathExists(root))
|
240
|
-
return;
|
241
|
-
const files = await (0, util_1.ls)(root);
|
242
|
-
const promises = files.map(async (f) => {
|
243
|
-
if (['bin', 'current', this.config.version].includes(path.basename(f.path)))
|
244
|
-
return;
|
245
|
-
const mtime = f.stat.mtime;
|
246
|
-
mtime.setHours(mtime.getHours() + (42 * 24));
|
247
|
-
if (mtime < new Date()) {
|
248
|
-
await fs.remove(f.path);
|
249
|
-
}
|
250
|
-
});
|
251
|
-
for (const p of promises)
|
252
|
-
await p; // eslint-disable-line no-await-in-loop
|
253
|
-
await this.logChop();
|
254
|
-
}
|
255
|
-
catch (error) {
|
256
|
-
cli_ux_1.default.warn(error);
|
257
|
-
}
|
258
|
-
}
|
259
|
-
async touch() {
|
260
|
-
// touch the client so it won't be tidied up right away
|
261
|
-
try {
|
262
|
-
const p = path.join(this.clientRoot, this.config.version);
|
263
|
-
this.debug('touching client at', p);
|
264
|
-
if (!await fs.pathExists(p))
|
265
|
-
return;
|
266
|
-
await fs.utimes(p, new Date(), new Date());
|
267
|
-
}
|
268
|
-
catch (error) {
|
269
|
-
this.warn(error);
|
270
|
-
}
|
271
|
-
}
|
272
|
-
async reexec() {
|
273
|
-
cli_ux_1.default.action.stop();
|
274
|
-
return new Promise((_, reject) => {
|
275
|
-
this.debug('restarting CLI after update', this.clientBin);
|
276
|
-
spawn(this.clientBin, ['update'], {
|
277
|
-
stdio: 'inherit',
|
278
|
-
env: Object.assign(Object.assign({}, process.env), { [this.config.scopedEnvVarKey('HIDE_UPDATED_MESSAGE')]: '1' }),
|
279
|
-
})
|
280
|
-
.on('error', reject)
|
281
|
-
.on('close', (status) => {
|
282
|
-
try {
|
283
|
-
if (status > 0)
|
284
|
-
this.exit(status);
|
285
|
-
}
|
286
|
-
catch (error) {
|
287
|
-
reject(error);
|
288
|
-
}
|
289
|
-
});
|
290
|
-
});
|
291
|
-
}
|
292
|
-
async createBin(version) {
|
293
|
-
const dst = this.clientBin;
|
294
|
-
const { bin } = this.config;
|
295
|
-
const binPathEnvVar = this.config.scopedEnvVarKey('BINPATH');
|
296
|
-
const redirectedEnvVar = this.config.scopedEnvVarKey('REDIRECTED');
|
297
|
-
if (this.config.windows) {
|
298
|
-
const body = `@echo off
|
299
|
-
setlocal enableextensions
|
300
|
-
set ${redirectedEnvVar}=1
|
301
|
-
set ${binPathEnvVar}=%~dp0${bin}
|
302
|
-
"%~dp0..\\${version}\\bin\\${bin}.cmd" %*
|
303
|
-
`;
|
304
|
-
await fs.outputFile(dst, body);
|
305
|
-
}
|
306
|
-
else {
|
307
|
-
/* eslint-disable no-useless-escape */
|
308
|
-
const body = `#!/usr/bin/env bash
|
309
|
-
set -e
|
310
|
-
get_script_dir () {
|
311
|
-
SOURCE="\${BASH_SOURCE[0]}"
|
312
|
-
# While $SOURCE is a symlink, resolve it
|
313
|
-
while [ -h "$SOURCE" ]; do
|
314
|
-
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
315
|
-
SOURCE="$( readlink "$SOURCE" )"
|
316
|
-
# If $SOURCE was a relative symlink (so no "/" as prefix, need to resolve it relative to the symlink base directory
|
317
|
-
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE"
|
318
|
-
done
|
319
|
-
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
320
|
-
echo "$DIR"
|
321
|
-
}
|
322
|
-
DIR=$(get_script_dir)
|
323
|
-
${binPathEnvVar}="\$DIR/${bin}" ${redirectedEnvVar}=1 "$DIR/../${version}/bin/${bin}" "$@"
|
324
|
-
`;
|
325
|
-
/* eslint-enable no-useless-escape */
|
326
|
-
await fs.remove(dst);
|
327
|
-
await fs.outputFile(dst, body);
|
328
|
-
await fs.chmod(dst, 0o755);
|
329
|
-
await fs.remove(path.join(this.clientRoot, 'current'));
|
330
|
-
await fs.symlink(`./${version}`, path.join(this.clientRoot, 'current'));
|
331
|
-
}
|
332
|
-
}
|
333
|
-
async ensureClientDir() {
|
334
|
-
try {
|
335
|
-
await fs.mkdirp(this.clientRoot);
|
336
|
-
}
|
337
|
-
catch (error) {
|
338
|
-
if (error.code === 'EEXIST') {
|
339
|
-
// for some reason the client directory is sometimes a file
|
340
|
-
// if so, this happens. Delete it and recreate
|
341
|
-
await fs.remove(this.clientRoot);
|
342
|
-
await fs.mkdirp(this.clientRoot);
|
343
|
-
}
|
344
|
-
else {
|
345
|
-
throw error;
|
346
|
-
}
|
347
|
-
}
|
18
|
+
const updateCli = new update_1.default({ channel: args.channel, autoUpdate: flags.autoupdate, fromLocal: flags['from-local'], config: this.config, exit: this.exit, getPinToVersion: getPinToVersion });
|
19
|
+
return updateCli.runUpdate();
|
348
20
|
}
|
349
21
|
}
|
350
22
|
exports.default = UpdateCommand;
|
package/lib/update.d.ts
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
import { Config } from '@oclif/core';
|
2
|
+
export interface UpdateCliOptions {
|
3
|
+
channel?: string;
|
4
|
+
autoUpdate: boolean;
|
5
|
+
fromLocal: boolean;
|
6
|
+
config: Config;
|
7
|
+
exit: any;
|
8
|
+
getPinToVersion: () => Promise<string>;
|
9
|
+
}
|
10
|
+
export default class UpdateCli {
|
11
|
+
private options;
|
12
|
+
private channel;
|
13
|
+
private currentVersion?;
|
14
|
+
private updatedVersion;
|
15
|
+
private readonly clientRoot;
|
16
|
+
private readonly clientBin;
|
17
|
+
constructor(options: UpdateCliOptions);
|
18
|
+
runUpdate(): Promise<void>;
|
19
|
+
private fetchManifest;
|
20
|
+
private downloadAndExtract;
|
21
|
+
private update;
|
22
|
+
private updateToExistingVersion;
|
23
|
+
private skipUpdate;
|
24
|
+
private determineChannel;
|
25
|
+
private determineCurrentVersion;
|
26
|
+
private s3ChannelManifestKey;
|
27
|
+
private setChannel;
|
28
|
+
private logChop;
|
29
|
+
private mtime;
|
30
|
+
private debounce;
|
31
|
+
private tidy;
|
32
|
+
private touch;
|
33
|
+
private reexec;
|
34
|
+
private createBin;
|
35
|
+
private ensureClientDir;
|
36
|
+
}
|
package/lib/update.js
ADDED
@@ -0,0 +1,331 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
const color_1 = require("@oclif/color");
|
4
|
+
const cli_ux_1 = require("cli-ux");
|
5
|
+
const spawn = require("cross-spawn");
|
6
|
+
const fs = require("fs-extra");
|
7
|
+
const _ = require("lodash");
|
8
|
+
const path = require("path");
|
9
|
+
const tar_1 = require("./tar");
|
10
|
+
const util_1 = require("./util");
|
11
|
+
class UpdateCli {
|
12
|
+
constructor(options) {
|
13
|
+
this.options = options;
|
14
|
+
this.clientRoot = this.options.config.scopedEnvVar('OCLIF_CLIENT_HOME') || path.join(this.options.config.dataDir, 'client');
|
15
|
+
this.clientBin = path.join(this.clientRoot, 'bin', this.options.config.windows ? `${this.options.config.bin}.cmd` : this.options.config.bin);
|
16
|
+
}
|
17
|
+
async runUpdate() {
|
18
|
+
if (this.options.autoUpdate)
|
19
|
+
await this.debounce();
|
20
|
+
this.channel = this.options.channel || await this.determineChannel();
|
21
|
+
if (this.options.fromLocal) {
|
22
|
+
await this.ensureClientDir();
|
23
|
+
cli_ux_1.default.debug(`Looking for locally installed versions at ${this.clientRoot}`);
|
24
|
+
// Do not show known non-local version folder names, bin and current.
|
25
|
+
const versions = fs.readdirSync(this.clientRoot).filter(dirOrFile => dirOrFile !== 'bin' && dirOrFile !== 'current');
|
26
|
+
if (versions.length === 0)
|
27
|
+
throw new Error('No locally installed versions found.');
|
28
|
+
cli_ux_1.default.log(`Found versions: \n${versions.map(version => ` ${version}`).join('\n')}\n`);
|
29
|
+
const pinToVersion = await this.options.getPinToVersion();
|
30
|
+
if (!versions.includes(pinToVersion))
|
31
|
+
throw new Error(`Version ${pinToVersion} not found in the locally installed versions.`);
|
32
|
+
if (!await fs.pathExists(path.join(this.clientRoot, pinToVersion))) {
|
33
|
+
throw new Error(`Version ${pinToVersion} is not already installed at ${this.clientRoot}.`);
|
34
|
+
}
|
35
|
+
cli_ux_1.default.action.start(`${this.options.config.name}: Updating CLI`);
|
36
|
+
cli_ux_1.default.debug(`switching to existing version ${pinToVersion}`);
|
37
|
+
this.updateToExistingVersion(pinToVersion);
|
38
|
+
cli_ux_1.default.log();
|
39
|
+
cli_ux_1.default.log(`Updating to an already installed version will not update the channel. If autoupdate is enabled, the CLI will eventually be updated back to ${this.channel}.`);
|
40
|
+
}
|
41
|
+
else {
|
42
|
+
cli_ux_1.default.action.start(`${this.options.config.name}: Updating CLI`);
|
43
|
+
await this.options.config.runHook('preupdate', { channel: this.channel });
|
44
|
+
const manifest = await this.fetchManifest();
|
45
|
+
this.currentVersion = await this.determineCurrentVersion();
|
46
|
+
this.updatedVersion = manifest.sha ? `${manifest.version}-${manifest.sha}` : manifest.version;
|
47
|
+
const reason = await this.skipUpdate();
|
48
|
+
if (reason)
|
49
|
+
cli_ux_1.default.action.stop(reason || 'done');
|
50
|
+
else
|
51
|
+
await this.update(manifest);
|
52
|
+
cli_ux_1.default.debug('tidy');
|
53
|
+
await this.tidy();
|
54
|
+
await this.options.config.runHook('update', { channel: this.channel });
|
55
|
+
}
|
56
|
+
cli_ux_1.default.debug('done');
|
57
|
+
cli_ux_1.default.action.stop();
|
58
|
+
}
|
59
|
+
async fetchManifest() {
|
60
|
+
const http = require('http-call').HTTP;
|
61
|
+
cli_ux_1.default.action.status = 'fetching manifest';
|
62
|
+
try {
|
63
|
+
const url = this.options.config.s3Url(this.options.config.s3Key('manifest', {
|
64
|
+
channel: this.channel,
|
65
|
+
platform: this.options.config.platform,
|
66
|
+
arch: this.options.config.arch,
|
67
|
+
}));
|
68
|
+
const { body } = await http.get(url);
|
69
|
+
// in case the content-type is not set, parse as a string
|
70
|
+
// this will happen if uploading without `oclif-dev publish`
|
71
|
+
if (typeof body === 'string') {
|
72
|
+
return JSON.parse(body);
|
73
|
+
}
|
74
|
+
return body;
|
75
|
+
}
|
76
|
+
catch (error) {
|
77
|
+
if (error.statusCode === 403)
|
78
|
+
throw new Error(`HTTP 403: Invalid channel ${this.channel}`);
|
79
|
+
throw error;
|
80
|
+
}
|
81
|
+
}
|
82
|
+
async downloadAndExtract(output, manifest, channel) {
|
83
|
+
const { version } = manifest;
|
84
|
+
const filesize = (n) => {
|
85
|
+
const [num, suffix] = require('filesize')(n, { output: 'array' });
|
86
|
+
return num.toFixed(1) + ` ${suffix}`;
|
87
|
+
};
|
88
|
+
const http = require('http-call').HTTP;
|
89
|
+
const gzUrl = manifest.gz || this.options.config.s3Url(this.options.config.s3Key('versioned', {
|
90
|
+
version,
|
91
|
+
channel,
|
92
|
+
bin: this.options.config.bin,
|
93
|
+
platform: this.options.config.platform,
|
94
|
+
arch: this.options.config.arch,
|
95
|
+
ext: 'gz',
|
96
|
+
}));
|
97
|
+
const { response: stream } = await http.stream(gzUrl);
|
98
|
+
stream.pause();
|
99
|
+
const baseDir = manifest.baseDir || this.options.config.s3Key('baseDir', {
|
100
|
+
version,
|
101
|
+
channel,
|
102
|
+
bin: this.options.config.bin,
|
103
|
+
platform: this.options.config.platform,
|
104
|
+
arch: this.options.config.arch,
|
105
|
+
});
|
106
|
+
const extraction = (0, tar_1.extract)(stream, baseDir, output, manifest.sha256gz);
|
107
|
+
// to-do: use cli.action.type
|
108
|
+
if (cli_ux_1.default.action.frames) {
|
109
|
+
// if spinner action
|
110
|
+
const total = parseInt(stream.headers['content-length'], 10);
|
111
|
+
let current = 0;
|
112
|
+
const updateStatus = _.throttle((newStatus) => {
|
113
|
+
cli_ux_1.default.action.status = newStatus;
|
114
|
+
}, 250, { leading: true, trailing: false });
|
115
|
+
stream.on('data', data => {
|
116
|
+
current += data.length;
|
117
|
+
updateStatus(`${filesize(current)}/${filesize(total)}`);
|
118
|
+
});
|
119
|
+
}
|
120
|
+
stream.resume();
|
121
|
+
await extraction;
|
122
|
+
}
|
123
|
+
async update(manifest, channel = 'stable') {
|
124
|
+
cli_ux_1.default.action.start(`${this.options.config.name}: Updating CLI from ${color_1.default.green(this.currentVersion)} to ${color_1.default.green(this.updatedVersion)}${channel === 'stable' ? '' : ' (' + color_1.default.yellow(channel) + ')'}`);
|
125
|
+
await this.ensureClientDir();
|
126
|
+
const output = path.join(this.clientRoot, this.updatedVersion);
|
127
|
+
if (!await fs.pathExists(output)) {
|
128
|
+
await this.downloadAndExtract(output, manifest, channel);
|
129
|
+
}
|
130
|
+
await this.setChannel();
|
131
|
+
await this.createBin(this.updatedVersion);
|
132
|
+
await this.touch();
|
133
|
+
await this.reexec();
|
134
|
+
}
|
135
|
+
async updateToExistingVersion(version) {
|
136
|
+
await this.createBin(version);
|
137
|
+
await this.touch();
|
138
|
+
}
|
139
|
+
async skipUpdate() {
|
140
|
+
if (!this.options.config.binPath) {
|
141
|
+
const instructions = this.options.config.scopedEnvVar('UPDATE_INSTRUCTIONS');
|
142
|
+
if (instructions)
|
143
|
+
cli_ux_1.default.warn(instructions);
|
144
|
+
return 'not updatable';
|
145
|
+
}
|
146
|
+
if (this.currentVersion === this.updatedVersion) {
|
147
|
+
if (this.options.config.scopedEnvVar('HIDE_UPDATED_MESSAGE'))
|
148
|
+
return 'done';
|
149
|
+
return `already on latest version: ${this.currentVersion}`;
|
150
|
+
}
|
151
|
+
return false;
|
152
|
+
}
|
153
|
+
async determineChannel() {
|
154
|
+
const channelPath = path.join(this.options.config.dataDir, 'channel');
|
155
|
+
if (fs.existsSync(channelPath)) {
|
156
|
+
const channel = await fs.readFile(channelPath, 'utf8');
|
157
|
+
return String(channel).trim();
|
158
|
+
}
|
159
|
+
return this.options.config.channel || 'stable';
|
160
|
+
}
|
161
|
+
async determineCurrentVersion() {
|
162
|
+
try {
|
163
|
+
const currentVersion = await fs.readFile(this.clientBin, 'utf8');
|
164
|
+
const matches = currentVersion.match(/\.\.[/|\\](.+)[/|\\]bin/);
|
165
|
+
return matches ? matches[1] : this.options.config.version;
|
166
|
+
}
|
167
|
+
catch (error) {
|
168
|
+
cli_ux_1.default.debug(error);
|
169
|
+
}
|
170
|
+
return this.options.config.version;
|
171
|
+
}
|
172
|
+
s3ChannelManifestKey(bin, platform, arch, folder) {
|
173
|
+
let s3SubDir = folder || '';
|
174
|
+
if (s3SubDir !== '' && s3SubDir.slice(-1) !== '/')
|
175
|
+
s3SubDir = `${s3SubDir}/`;
|
176
|
+
return path.join(s3SubDir, 'channels', this.channel, `${bin}-${platform}-${arch}-buildmanifest`);
|
177
|
+
}
|
178
|
+
async setChannel() {
|
179
|
+
const channelPath = path.join(this.options.config.dataDir, 'channel');
|
180
|
+
fs.writeFile(channelPath, this.channel, 'utf8');
|
181
|
+
}
|
182
|
+
async logChop() {
|
183
|
+
try {
|
184
|
+
cli_ux_1.default.debug('log chop');
|
185
|
+
const logChopper = require('log-chopper').default;
|
186
|
+
await logChopper.chop(this.options.config.errlog);
|
187
|
+
}
|
188
|
+
catch (error) {
|
189
|
+
cli_ux_1.default.debug(error.message);
|
190
|
+
}
|
191
|
+
}
|
192
|
+
async mtime(f) {
|
193
|
+
const { mtime } = await fs.stat(f);
|
194
|
+
return mtime;
|
195
|
+
}
|
196
|
+
// when autoupdating, wait until the CLI isn't active
|
197
|
+
async debounce() {
|
198
|
+
let output = false;
|
199
|
+
const lastrunfile = path.join(this.options.config.cacheDir, 'lastrun');
|
200
|
+
const m = await this.mtime(lastrunfile);
|
201
|
+
m.setHours(m.getHours() + 1);
|
202
|
+
if (m > new Date()) {
|
203
|
+
const msg = `waiting until ${m.toISOString()} to update`;
|
204
|
+
if (output) {
|
205
|
+
cli_ux_1.default.debug(msg);
|
206
|
+
}
|
207
|
+
else {
|
208
|
+
await cli_ux_1.default.log(msg);
|
209
|
+
output = true;
|
210
|
+
}
|
211
|
+
await (0, util_1.wait)(60 * 1000); // wait 1 minute
|
212
|
+
return this.debounce();
|
213
|
+
}
|
214
|
+
cli_ux_1.default.log('time to update');
|
215
|
+
}
|
216
|
+
// removes any unused CLIs
|
217
|
+
async tidy() {
|
218
|
+
try {
|
219
|
+
const root = this.clientRoot;
|
220
|
+
if (!await fs.pathExists(root))
|
221
|
+
return;
|
222
|
+
const files = await (0, util_1.ls)(root);
|
223
|
+
const promises = files.map(async (f) => {
|
224
|
+
if (['bin', 'current', this.options.config.version].includes(path.basename(f.path)))
|
225
|
+
return;
|
226
|
+
const mtime = f.stat.mtime;
|
227
|
+
mtime.setHours(mtime.getHours() + (42 * 24));
|
228
|
+
if (mtime < new Date()) {
|
229
|
+
await fs.remove(f.path);
|
230
|
+
}
|
231
|
+
});
|
232
|
+
for (const p of promises)
|
233
|
+
await p; // eslint-disable-line no-await-in-loop
|
234
|
+
await this.logChop();
|
235
|
+
}
|
236
|
+
catch (error) {
|
237
|
+
cli_ux_1.default.warn(error);
|
238
|
+
}
|
239
|
+
}
|
240
|
+
async touch() {
|
241
|
+
// touch the client so it won't be tidied up right away
|
242
|
+
try {
|
243
|
+
const p = path.join(this.clientRoot, this.options.config.version);
|
244
|
+
cli_ux_1.default.debug('touching client at', p);
|
245
|
+
if (!await fs.pathExists(p))
|
246
|
+
return;
|
247
|
+
await fs.utimes(p, new Date(), new Date());
|
248
|
+
}
|
249
|
+
catch (error) {
|
250
|
+
cli_ux_1.default.warn(error);
|
251
|
+
}
|
252
|
+
}
|
253
|
+
async reexec() {
|
254
|
+
cli_ux_1.default.action.stop();
|
255
|
+
return new Promise((_, reject) => {
|
256
|
+
cli_ux_1.default.debug('restarting CLI after update', this.clientBin);
|
257
|
+
spawn(this.clientBin, ['update'], {
|
258
|
+
stdio: 'inherit',
|
259
|
+
env: Object.assign(Object.assign({}, process.env), { [this.options.config.scopedEnvVarKey('HIDE_UPDATED_MESSAGE')]: '1' }),
|
260
|
+
})
|
261
|
+
.on('error', reject)
|
262
|
+
.on('close', (status) => {
|
263
|
+
try {
|
264
|
+
if (status > 0)
|
265
|
+
this.options.exit(status);
|
266
|
+
}
|
267
|
+
catch (error) {
|
268
|
+
reject(error);
|
269
|
+
}
|
270
|
+
});
|
271
|
+
});
|
272
|
+
}
|
273
|
+
async createBin(version) {
|
274
|
+
const dst = this.clientBin;
|
275
|
+
const { bin } = this.options.config;
|
276
|
+
const binPathEnvVar = this.options.config.scopedEnvVarKey('BINPATH');
|
277
|
+
const redirectedEnvVar = this.options.config.scopedEnvVarKey('REDIRECTED');
|
278
|
+
if (this.options.config.windows) {
|
279
|
+
const body = `@echo off
|
280
|
+
setlocal enableextensions
|
281
|
+
set ${redirectedEnvVar}=1
|
282
|
+
set ${binPathEnvVar}=%~dp0${bin}
|
283
|
+
"%~dp0..\\${version}\\bin\\${bin}.cmd" %*
|
284
|
+
`;
|
285
|
+
await fs.outputFile(dst, body);
|
286
|
+
}
|
287
|
+
else {
|
288
|
+
/* eslint-disable no-useless-escape */
|
289
|
+
const body = `#!/usr/bin/env bash
|
290
|
+
set -e
|
291
|
+
get_script_dir () {
|
292
|
+
SOURCE="\${BASH_SOURCE[0]}"
|
293
|
+
# While $SOURCE is a symlink, resolve it
|
294
|
+
while [ -h "$SOURCE" ]; do
|
295
|
+
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
296
|
+
SOURCE="$( readlink "$SOURCE" )"
|
297
|
+
# If $SOURCE was a relative symlink (so no "/" as prefix, need to resolve it relative to the symlink base directory
|
298
|
+
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE"
|
299
|
+
done
|
300
|
+
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
301
|
+
echo "$DIR"
|
302
|
+
}
|
303
|
+
DIR=$(get_script_dir)
|
304
|
+
${binPathEnvVar}="\$DIR/${bin}" ${redirectedEnvVar}=1 "$DIR/../${version}/bin/${bin}" "$@"
|
305
|
+
`;
|
306
|
+
/* eslint-enable no-useless-escape */
|
307
|
+
await fs.remove(dst);
|
308
|
+
await fs.outputFile(dst, body);
|
309
|
+
await fs.chmod(dst, 0o755);
|
310
|
+
await fs.remove(path.join(this.clientRoot, 'current'));
|
311
|
+
await fs.symlink(`./${version}`, path.join(this.clientRoot, 'current'));
|
312
|
+
}
|
313
|
+
}
|
314
|
+
async ensureClientDir() {
|
315
|
+
try {
|
316
|
+
await fs.mkdirp(this.clientRoot);
|
317
|
+
}
|
318
|
+
catch (error) {
|
319
|
+
if (error.code === 'EEXIST') {
|
320
|
+
// for some reason the client directory is sometimes a file
|
321
|
+
// if so, this happens. Delete it and recreate
|
322
|
+
await fs.remove(this.clientRoot);
|
323
|
+
await fs.mkdirp(this.clientRoot);
|
324
|
+
}
|
325
|
+
else {
|
326
|
+
throw error;
|
327
|
+
}
|
328
|
+
}
|
329
|
+
}
|
330
|
+
}
|
331
|
+
exports.default = UpdateCli;
|
package/oclif.manifest.json
CHANGED
@@ -1 +1 @@
|
|
1
|
-
{"version":"2.
|
1
|
+
{"version":"2.1.0","commands":{"update":{"id":"update","description":"update the <%= config.bin %> CLI","strict":true,"pluginName":"@oclif/plugin-update","pluginAlias":"@oclif/plugin-update","pluginType":"core","aliases":[],"flags":{"autoupdate":{"name":"autoupdate","type":"boolean","hidden":true,"allowNo":false},"from-local":{"name":"from-local","type":"boolean","description":"interactively choose an already installed version","allowNo":false}},"args":[{"name":"channel"}]}}}
|
package/package.json
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
{
|
2
2
|
"name": "@oclif/plugin-update",
|
3
|
-
"version": "2.
|
3
|
+
"version": "2.1.0",
|
4
4
|
"author": "Jeff Dickey @jdxcode",
|
5
5
|
"bugs": "https://github.com/oclif/plugin-update/issues",
|
6
6
|
"dependencies": {
|
7
7
|
"@oclif/color": "^0.1.0",
|
8
|
-
"@oclif/core": "^0.
|
8
|
+
"@oclif/core": "^1.0.1",
|
9
9
|
"@types/semver": "^7.3.4",
|
10
10
|
"cli-ux": "^5.5.1",
|
11
11
|
"cross-spawn": "^7.0.3",
|
@@ -19,7 +19,6 @@
|
|
19
19
|
"tar-fs": "^2.1.1"
|
20
20
|
},
|
21
21
|
"devDependencies": {
|
22
|
-
"@oclif/dev-cli": "^1.26.0",
|
23
22
|
"@oclif/plugin-help": "^5.1.0",
|
24
23
|
"@oclif/test": "^1.2.8",
|
25
24
|
"@types/chai": "^4.2.15",
|
@@ -38,8 +37,10 @@
|
|
38
37
|
"eslint-config-oclif-typescript": "^0.2.0",
|
39
38
|
"globby": "^11.0.2",
|
40
39
|
"mocha": "^8.3.2",
|
40
|
+
"nock": "^13.2.1",
|
41
41
|
"oclif": "^2.0.0-main.5",
|
42
42
|
"qqjs": "^0.3.11",
|
43
|
+
"sinon": "^12.0.1",
|
43
44
|
"ts-node": "^9.1.1",
|
44
45
|
"tslib": "^2.1.0",
|
45
46
|
"typescript": "4.4.3"
|