@oclif/plugin-update 2.0.0 → 2.1.3

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 CHANGED
@@ -2,6 +2,29 @@
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.3](https://github.com/oclif/plugin-update/compare/v2.1.2...v2.1.3) (2021-12-09)
6
+
7
+ ### [2.1.2](https://github.com/oclif/plugin-update/compare/v2.1.1...v2.1.2) (2021-12-08)
8
+
9
+
10
+ ### Bug Fixes
11
+
12
+ * bump deps-main ([#343](https://github.com/oclif/plugin-update/issues/343)) ([8f110df](https://github.com/oclif/plugin-update/commit/8f110dfe7a69931466cd30510014c99eee624e50))
13
+
14
+ ### [2.1.1](https://github.com/oclif/plugin-update/compare/v2.1.0...v2.1.1) (2021-12-02)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * apply v2 deps ([#342](https://github.com/oclif/plugin-update/issues/342)) ([ea1abfa](https://github.com/oclif/plugin-update/commit/ea1abfab9840523636dbf55f183cbf8b6e46d501))
20
+
21
+ ## [2.1.0](https://github.com/oclif/plugin-update/compare/v2.0.0...v2.1.0) (2021-11-23)
22
+
23
+
24
+ ### Features
25
+
26
+ * remove legacy update logic ([#314](https://github.com/oclif/plugin-update/issues/314)) ([5241c20](https://github.com/oclif/plugin-update/commit/5241c20dc4eea215bfdedfccecc2e4acf3d3465b))
27
+
5
28
  ## [1.5.0](https://github.com/oclif/plugin-update/compare/v1.4.0...v1.5.0) (2021-08-05)
6
29
 
7
30
  ## [1.4.0](https://github.com/oclif/plugin-update/compare/v1.4.0-3...v1.4.0) (2021-08-05)
package/README.md CHANGED
@@ -2,10 +2,10 @@
2
2
  ====================
3
3
 
4
4
  [![Version](https://img.shields.io/npm/v/@oclif/plugin-update.svg)](https://npmjs.org/package/@oclif/plugin-update)
5
- [![CircleCI](https://circleci.com/gh/oclif/plugin-update/tree/master.svg?style=shield)](https://circleci.com/gh/oclif/plugin-update/tree/master)
6
- [![Appveyor CI](https://ci.appveyor.com/api/projects/status/github/oclif/plugin-update?branch=master&svg=true)](https://ci.appveyor.com/project/oclif/plugin-update/branch/master)
5
+ [![CircleCI](https://circleci.com/gh/oclif/plugin-update/tree/main.svg?style=shield)](https://circleci.com/gh/oclif/plugin-update/tree/main)
6
+ [![Appveyor CI](https://ci.appveyor.com/api/projects/status/github/oclif/plugin-update?branch=main&svg=true)](https://ci.appveyor.com/project/oclif/plugin-update/branch/main)
7
7
  [![Downloads/week](https://img.shields.io/npm/dw/@oclif/plugin-update.svg)](https://npmjs.org/package/@oclif/plugin-update)
8
- [![License](https://img.shields.io/npm/l/@oclif/plugin-update.svg)](https://github.com/oclif/plugin-update/blob/master/package.json)
8
+ [![License](https://img.shields.io/npm/l/@oclif/plugin-update.svg)](https://github.com/oclif/plugin-update/blob/main/package.json)
9
9
 
10
10
  <!-- toc -->
11
11
  * [Usage](#usage)
@@ -33,5 +33,5 @@ DESCRIPTION
33
33
  update the oclif-example CLI
34
34
  ```
35
35
 
36
- _See code: [src/commands/update.ts](https://github.com/oclif/plugin-update/blob/v2.0.0/src/commands/update.ts)_
36
+ _See code: [src/commands/update.ts](https://github.com/oclif/plugin-update/blob/v2.1.3/src/commands/update.ts)_
37
37
  <!-- commandsstop -->
@@ -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
  }
@@ -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 tar_1 = require("../tar");
11
- const util_1 = require("../util");
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
- this.autoupdate = Boolean(flags.autoupdate);
21
- if (this.autoupdate)
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/tar.js CHANGED
@@ -5,6 +5,19 @@ const fs = require("fs-extra");
5
5
  const path = require("path");
6
6
  const util_1 = require("./util");
7
7
  const debug = require('debug')('oclif-update');
8
+ const ignore = (_, header) => {
9
+ switch (header.type) {
10
+ case 'directory':
11
+ case 'file':
12
+ if (process.env.OCLIF_DEBUG_UPDATE_FILES)
13
+ debug(header.name);
14
+ return false;
15
+ case 'symlink':
16
+ return true;
17
+ default:
18
+ throw new Error(header.type);
19
+ }
20
+ };
8
21
  async function extract(stream, basename, output, sha) {
9
22
  const getTmp = () => `${output}.partial.${Math.random().toString().split('.')[1].slice(0, 5)}`;
10
23
  let tmp = getTmp();
@@ -36,19 +49,6 @@ async function extract(stream, basename, output, sha) {
36
49
  }
37
50
  else
38
51
  shaValidated = true;
39
- const ignore = (_, header) => {
40
- switch (header.type) {
41
- case 'directory':
42
- case 'file':
43
- if (process.env.OCLIF_DEBUG_UPDATE_FILES)
44
- debug(header.name);
45
- return false;
46
- case 'symlink':
47
- return true;
48
- default:
49
- throw new Error(header.type);
50
- }
51
- };
52
52
  const extract = tar.extract(tmp, { ignore });
53
53
  extract.on('error', reject);
54
54
  extract.on('finish', () => {
@@ -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, gz, sha256gz } = 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 = 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, sha256gz);
107
+ // to-do: use cli.action.type
108
+ if (cli_ux_1.default.action.frames) {
109
+ // if spinner action
110
+ const total = Number.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, windows } = this.options.config;
276
+ const binPathEnvVar = this.options.config.scopedEnvVarKey('BINPATH');
277
+ const redirectedEnvVar = this.options.config.scopedEnvVarKey('REDIRECTED');
278
+ if (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;
@@ -1 +1 @@
1
- {"version":"2.0.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"}]}}}
1
+ {"version":"2.1.3","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,13 +1,13 @@
1
1
  {
2
2
  "name": "@oclif/plugin-update",
3
- "version": "2.0.0",
4
- "author": "Jeff Dickey @jdxcode",
3
+ "version": "2.1.3",
4
+ "author": "Salesforce",
5
5
  "bugs": "https://github.com/oclif/plugin-update/issues",
6
6
  "dependencies": {
7
- "@oclif/color": "^0.1.0",
8
- "@oclif/core": "^0.5.39",
7
+ "@oclif/color": "^1.0.0",
8
+ "@oclif/core": "1.0.10",
9
9
  "@types/semver": "^7.3.4",
10
- "cli-ux": "^5.5.1",
10
+ "cli-ux": "6.0.6",
11
11
  "cross-spawn": "^7.0.3",
12
12
  "debug": "^4.3.1",
13
13
  "filesize": "^6.1.0",
@@ -19,27 +19,28 @@
19
19
  "tar-fs": "^2.1.1"
20
20
  },
21
21
  "devDependencies": {
22
- "@oclif/dev-cli": "^1.26.0",
23
- "@oclif/plugin-help": "^5.1.0",
24
- "@oclif/test": "^1.2.8",
22
+ "@oclif/plugin-help": "5.1.9",
23
+ "@oclif/test": "^2.0.2",
25
24
  "@types/chai": "^4.2.15",
26
25
  "@types/cross-spawn": "^6.0.2",
27
26
  "@types/execa": "^0.9.0",
28
27
  "@types/fs-extra": "^8.0.1",
29
28
  "@types/glob": "^7.1.3",
30
29
  "@types/lodash": "^4.14.168",
31
- "@types/mocha": "^8.2.2",
30
+ "@types/mocha": "^9",
32
31
  "@types/node": "^14.14.31",
33
32
  "@types/supports-color": "^7.2.0",
34
33
  "@types/write-json-file": "^3.2.1",
35
34
  "chai": "^4.3.4",
36
- "eslint": "^7.21.0",
37
- "eslint-config-oclif": "^3.1.0",
38
- "eslint-config-oclif-typescript": "^0.2.0",
35
+ "eslint": "^7.32.0",
36
+ "eslint-config-oclif": "^4",
37
+ "eslint-config-oclif-typescript": "^1.0.2",
39
38
  "globby": "^11.0.2",
40
- "mocha": "^8.3.2",
41
- "oclif": "^2.0.0-main.5",
39
+ "mocha": "^9",
40
+ "nock": "^13.2.1",
41
+ "oclif": "2.0.0-main.19",
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"