@oclif/plugin-update 2.1.3 → 3.0.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 CHANGED
@@ -2,6 +2,61 @@
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
+ ## [3.0.0](https://github.com/oclif/plugin-update/compare/v2.2.0...v3.0.0) (2022-02-11)
6
+
7
+
8
+ ### ⚠ BREAKING CHANGES
9
+
10
+ * rename --from-local to --local
11
+
12
+ * chore: update examples
13
+
14
+ * feat!: remove --local flag
15
+ * remove --local flag
16
+
17
+ * refactor: Updater
18
+
19
+ * fix: handle non-existent version index
20
+
21
+ * chore: code review
22
+
23
+ * chore: update test
24
+
25
+ * fix: skip preupdate hook if using --hard
26
+
27
+ * fix: update root after hard update
28
+
29
+ * fix: works with sf
30
+
31
+ * fix: replace --hard with --force
32
+
33
+ * chore: update examples
34
+
35
+ * chore: bump to 3.0.0
36
+
37
+ * BREAKING CHANGE: add new update features (#368) ([091c176](https://github.com/oclif/plugin-update/commit/091c176ef639a7a0c991ab1b726e832783670324)), closes [#368](https://github.com/oclif/plugin-update/issues/368)
38
+
39
+ ## [2.2.0](https://github.com/oclif/plugin-update/compare/v2.1.5...v2.2.0) (2022-01-28)
40
+
41
+
42
+ ### Features
43
+
44
+ * remove cli-ux ([#364](https://github.com/oclif/plugin-update/issues/364)) ([980c612](https://github.com/oclif/plugin-update/commit/980c6121846a9201da4071f6edc2afc0219751f6))
45
+
46
+ ### [2.1.5](https://github.com/oclif/plugin-update/compare/v2.1.4...v2.1.5) (2022-01-06)
47
+
48
+
49
+ ### Bug Fixes
50
+
51
+ * add main to package.json ([#361](https://github.com/oclif/plugin-update/issues/361)) ([9daade3](https://github.com/oclif/plugin-update/commit/9daade340a102a8e0a57ae8a278b3803daf32117))
52
+
53
+ ### [2.1.4](https://github.com/oclif/plugin-update/compare/v2.1.3...v2.1.4) (2022-01-06)
54
+
55
+
56
+ ### Bug Fixes
57
+
58
+ * bump @oclif/core ([#360](https://github.com/oclif/plugin-update/issues/360)) ([749b949](https://github.com/oclif/plugin-update/commit/749b949e8127a5b594f28fe7468c1e13440f9817))
59
+
5
60
  ### [2.1.3](https://github.com/oclif/plugin-update/compare/v2.1.2...v2.1.3) (2021-12-09)
6
61
 
7
62
  ### [2.1.2](https://github.com/oclif/plugin-update/compare/v2.1.1...v2.1.2) (2021-12-08)
package/README.md CHANGED
@@ -24,14 +24,34 @@ update the oclif-example CLI
24
24
 
25
25
  ```
26
26
  USAGE
27
- $ oclif-example update [CHANNEL] [--from-local]
27
+ $ oclif-example update [CHANNEL] [-a] [-v <value> | -i] [--force]
28
28
 
29
29
  FLAGS
30
- --from-local interactively choose an already installed version
30
+ -a, --available Install a specific version.
31
+ -i, --interactive Interactively select version to install. This is ignored if a channel is provided.
32
+ -v, --version=<value> Install a specific version.
33
+ --force Force a re-download of the requested version.
31
34
 
32
35
  DESCRIPTION
33
36
  update the oclif-example CLI
37
+
38
+ EXAMPLES
39
+ Update to the stable channel:
40
+
41
+ $ oclif-example update stable
42
+
43
+ Update to a specific version:
44
+
45
+ $ oclif-example update --version 1.0.0
46
+
47
+ Interactively select version:
48
+
49
+ $ oclif-example update --interactive
50
+
51
+ See available versions:
52
+
53
+ $ oclif-example update --available
34
54
  ```
35
55
 
36
- _See code: [src/commands/update.ts](https://github.com/oclif/plugin-update/blob/v2.1.3/src/commands/update.ts)_
56
+ _See code: [src/commands/update.ts](https://github.com/oclif/plugin-update/blob/v3.0.0/src/commands/update.ts)_
37
57
  <!-- commandsstop -->
@@ -5,12 +5,17 @@ export default class UpdateCommand extends Command {
5
5
  name: string;
6
6
  optional: boolean;
7
7
  }[];
8
+ static examples: {
9
+ description: string;
10
+ command: string;
11
+ }[];
8
12
  static flags: {
9
13
  autoupdate: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
10
- 'from-local': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
14
+ available: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
15
+ version: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined>;
16
+ interactive: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
17
+ force: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
11
18
  };
12
- private channel;
13
- private readonly clientRoot;
14
- private readonly clientBin;
15
19
  run(): Promise<void>;
20
+ private promptForVersion;
16
21
  }
@@ -1,28 +1,85 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
3
4
  const core_1 = require("@oclif/core");
4
- const cli_ux_1 = require("cli-ux");
5
- const path = require("path");
5
+ const inquirer_1 = require("inquirer");
6
+ const path = (0, tslib_1.__importStar)(require("path"));
7
+ const semver_1 = require("semver");
6
8
  const update_1 = require("../update");
7
- async function getPinToVersion() {
8
- return cli_ux_1.default.prompt('Enter a version to update to');
9
- }
10
9
  class UpdateCommand extends core_1.Command {
11
- constructor() {
12
- super(...arguments);
13
- this.clientRoot = this.config.scopedEnvVar('OCLIF_CLIENT_HOME') || path.join(this.config.dataDir, 'client');
14
- this.clientBin = path.join(this.clientRoot, 'bin', this.config.windows ? `${this.config.bin}.cmd` : this.config.bin);
15
- }
16
10
  async run() {
17
11
  const { args, flags } = await this.parse(UpdateCommand);
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();
12
+ const updater = new update_1.Updater(this.config);
13
+ if (flags.available) {
14
+ const index = await updater.fetchVersionIndex();
15
+ const allVersions = (0, semver_1.sort)(Object.keys(index)).reverse();
16
+ const localVersions = await updater.findLocalVersions();
17
+ const table = allVersions.map(version => {
18
+ const location = localVersions.find(l => path.basename(l).startsWith(version)) || index[version];
19
+ return { version, location };
20
+ });
21
+ core_1.CliUx.ux.table(table, { version: {}, location: {} });
22
+ return;
23
+ }
24
+ if (args.channel && flags.version) {
25
+ this.error('You cannot specifiy both a version and a channel.');
26
+ }
27
+ return updater.runUpdate({
28
+ channel: args.channel,
29
+ autoUpdate: flags.autoupdate,
30
+ force: flags.force,
31
+ version: flags.interactive ? await this.promptForVersion(updater) : flags.version,
32
+ });
33
+ }
34
+ async promptForVersion(updater) {
35
+ const choices = (0, semver_1.sort)(Object.keys(await updater.fetchVersionIndex())).reverse();
36
+ const { version } = await (0, inquirer_1.prompt)({
37
+ name: 'version',
38
+ message: 'Select a version to update to',
39
+ type: 'list',
40
+ choices: [...choices, new inquirer_1.Separator()],
41
+ });
42
+ return version;
20
43
  }
21
44
  }
22
45
  exports.default = UpdateCommand;
23
46
  UpdateCommand.description = 'update the <%= config.bin %> CLI';
24
47
  UpdateCommand.args = [{ name: 'channel', optional: true }];
48
+ UpdateCommand.examples = [
49
+ {
50
+ description: 'Update to the stable channel:',
51
+ command: '<%= config.bin %> <%= command.id %> stable',
52
+ },
53
+ {
54
+ description: 'Update to a specific version:',
55
+ command: '<%= config.bin %> <%= command.id %> --version 1.0.0',
56
+ },
57
+ {
58
+ description: 'Interactively select version:',
59
+ command: '<%= config.bin %> <%= command.id %> --interactive',
60
+ },
61
+ {
62
+ description: 'See available versions:',
63
+ command: '<%= config.bin %> <%= command.id %> --available',
64
+ },
65
+ ];
25
66
  UpdateCommand.flags = {
26
67
  autoupdate: core_1.Flags.boolean({ hidden: true }),
27
- 'from-local': core_1.Flags.boolean({ description: 'interactively choose an already installed version' }),
68
+ available: core_1.Flags.boolean({
69
+ char: 'a',
70
+ description: 'Install a specific version.',
71
+ }),
72
+ version: core_1.Flags.string({
73
+ char: 'v',
74
+ description: 'Install a specific version.',
75
+ exclusive: ['interactive'],
76
+ }),
77
+ interactive: core_1.Flags.boolean({
78
+ char: 'i',
79
+ description: 'Interactively select version to install. This is ignored if a channel is provided.',
80
+ exclusive: ['version'],
81
+ }),
82
+ force: core_1.Flags.boolean({
83
+ description: 'Force a re-download of the requested version.',
84
+ }),
28
85
  };
package/lib/hooks/init.js CHANGED
@@ -1,11 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.init = void 0;
4
- const cli_ux_1 = require("cli-ux");
5
- const spawn = require("cross-spawn");
6
- const fs = require("fs-extra");
7
- const path = require("path");
4
+ const tslib_1 = require("tslib");
5
+ const core_1 = require("@oclif/core");
6
+ const cross_spawn_1 = (0, tslib_1.__importDefault)(require("cross-spawn"));
7
+ const fs = (0, tslib_1.__importStar)(require("fs-extra"));
8
+ const path = (0, tslib_1.__importStar)(require("path"));
8
9
  const util_1 = require("../util");
10
+ // eslint-disable-next-line unicorn/prefer-module
9
11
  const debug = require('debug')('cli:updater');
10
12
  function timestamp(msg) {
11
13
  return `[${new Date().toISOString()}] ${msg}`;
@@ -36,7 +38,7 @@ const init = async function (opts) {
36
38
  }
37
39
  catch (error) {
38
40
  if (error.code !== 'ENOENT')
39
- cli_ux_1.default.error(error.stack);
41
+ core_1.CliUx.ux.error(error.stack);
40
42
  if (global.testing)
41
43
  return false;
42
44
  debug('autoupdate ENOENT');
@@ -54,7 +56,7 @@ const init = async function (opts) {
54
56
  debug(`spawning autoupdate on ${binPath}`);
55
57
  const fd = await fs.open(autoupdatelogfile, 'a');
56
58
  fs.write(fd, timestamp(`starting \`${binPath} update --autoupdate\` from ${process.argv.slice(1, 3).join(' ')}\n`));
57
- spawn(binPath, ['update', '--autoupdate'], {
59
+ (0, cross_spawn_1.default)(binPath, ['update', '--autoupdate'], {
58
60
  detached: !this.config.windows,
59
61
  stdio: ['ignore', fd, fd],
60
62
  env: autoupdateEnv,
package/lib/tar.js CHANGED
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.extract = void 0;
4
- const fs = require("fs-extra");
5
- const path = require("path");
4
+ const tslib_1 = require("tslib");
5
+ const fs = (0, tslib_1.__importStar)(require("fs-extra"));
6
+ const path = (0, tslib_1.__importStar)(require("path"));
6
7
  const util_1 = require("./util");
7
8
  const debug = require('debug')('oclif-update');
8
9
  const ignore = (_, header) => {
package/lib/update.d.ts CHANGED
@@ -1,36 +1,43 @@
1
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>;
2
+ export declare namespace Updater {
3
+ type Options = {
4
+ autoUpdate: boolean;
5
+ channel?: string | undefined;
6
+ version?: string | undefined;
7
+ force?: boolean;
8
+ };
9
+ type VersionIndex = Record<string, string>;
9
10
  }
10
- export default class UpdateCli {
11
- private options;
12
- private channel;
13
- private currentVersion?;
14
- private updatedVersion;
11
+ export declare class Updater {
12
+ private config;
15
13
  private readonly clientRoot;
16
14
  private readonly clientBin;
17
- constructor(options: UpdateCliOptions);
18
- runUpdate(): Promise<void>;
15
+ constructor(config: Config);
16
+ runUpdate(options: Updater.Options): Promise<void>;
17
+ findLocalVersions(): Promise<string[]>;
18
+ fetchVersionIndex(): Promise<Updater.VersionIndex>;
19
+ private ensureClientDir;
20
+ private composeS3SubDir;
21
+ private s3ChannelManifestKey;
22
+ private s3VersionManifestKey;
23
+ private s3VersionIndexKey;
24
+ private fetchChannelManifest;
25
+ private fetchVersionManifest;
19
26
  private fetchManifest;
20
27
  private downloadAndExtract;
21
28
  private update;
22
29
  private updateToExistingVersion;
23
- private skipUpdate;
30
+ private notUpdatable;
31
+ private alreadyOnVersion;
24
32
  private determineChannel;
25
33
  private determineCurrentVersion;
26
- private s3ChannelManifestKey;
34
+ private findLocalVersion;
27
35
  private setChannel;
28
36
  private logChop;
29
37
  private mtime;
30
38
  private debounce;
31
39
  private tidy;
32
40
  private touch;
33
- private reexec;
41
+ private refreshConfig;
34
42
  private createBin;
35
- private ensureClientDir;
36
43
  }
package/lib/update.js CHANGED
@@ -1,116 +1,190 @@
1
1
  "use strict";
2
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");
3
+ exports.Updater = void 0;
4
+ const tslib_1 = require("tslib");
5
+ /* eslint-disable unicorn/prefer-module */
6
+ const color_1 = (0, tslib_1.__importDefault)(require("@oclif/color"));
7
+ const core_1 = require("@oclif/core");
8
+ const fs = (0, tslib_1.__importStar)(require("fs-extra"));
9
+ const http_call_1 = (0, tslib_1.__importDefault)(require("http-call"));
10
+ const path = (0, tslib_1.__importStar)(require("path"));
11
+ const lodash_throttle_1 = (0, tslib_1.__importDefault)(require("lodash.throttle"));
12
+ const filesize_1 = (0, tslib_1.__importDefault)(require("filesize"));
9
13
  const tar_1 = require("./tar");
10
14
  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);
15
+ const filesize = (n) => {
16
+ const [num, suffix] = (0, filesize_1.default)(n, { output: 'array' });
17
+ return Number.parseFloat(num).toFixed(1) + ` ${suffix}`;
18
+ };
19
+ class Updater {
20
+ constructor(config) {
21
+ this.config = config;
22
+ this.clientRoot = config.scopedEnvVar('OCLIF_CLIENT_HOME') || path.join(config.dataDir, 'client');
23
+ this.clientBin = path.join(this.clientRoot, 'bin', config.windows ? `${config.bin}.cmd` : config.bin);
16
24
  }
17
- async runUpdate() {
18
- if (this.options.autoUpdate)
25
+ async runUpdate(options) {
26
+ const { autoUpdate, version, force = false } = options;
27
+ if (autoUpdate)
19
28
  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}.`);
29
+ core_1.CliUx.ux.action.start(`${this.config.name}: Updating CLI`);
30
+ if (this.notUpdatable()) {
31
+ core_1.CliUx.ux.action.stop('not updatable');
32
+ return;
33
+ }
34
+ const channel = options.channel || await this.determineChannel();
35
+ const current = await this.determineCurrentVersion();
36
+ if (version) {
37
+ const localVersion = force ? null : await this.findLocalVersion(version);
38
+ if (this.alreadyOnVersion(current, localVersion || null)) {
39
+ core_1.CliUx.ux.action.stop(this.config.scopedEnvVar('HIDE_UPDATED_MESSAGE') ? 'done' : `already on version ${current}`);
40
+ return;
41
+ }
42
+ await this.config.runHook('preupdate', { channel, version });
43
+ if (localVersion) {
44
+ await this.updateToExistingVersion(current, localVersion);
34
45
  }
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}.`);
46
+ else {
47
+ const index = await this.fetchVersionIndex();
48
+ const url = index[version];
49
+ if (!url) {
50
+ throw new Error(`${version} not found in index:\n${Object.keys(index).join(', ')}`);
51
+ }
52
+ const manifest = await this.fetchVersionManifest(version, url);
53
+ const updated = manifest.sha ? `${manifest.version}-${manifest.sha}` : manifest.version;
54
+ await this.update(manifest, current, updated, force, channel);
55
+ }
56
+ await this.config.runHook('update', { channel, version });
57
+ core_1.CliUx.ux.action.stop();
58
+ core_1.CliUx.ux.log();
59
+ core_1.CliUx.ux.log(`Updating to a specific version will not update the channel. If autoupdate is enabled, the CLI will eventually be updated back to ${channel}.`);
40
60
  }
41
61
  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 });
62
+ const manifest = await this.fetchChannelManifest(channel);
63
+ const updated = manifest.sha ? `${manifest.version}-${manifest.sha}` : manifest.version;
64
+ if (!force && this.alreadyOnVersion(current, updated)) {
65
+ core_1.CliUx.ux.action.stop(this.config.scopedEnvVar('HIDE_UPDATED_MESSAGE') ? 'done' : `already on version ${current}`);
66
+ }
67
+ else {
68
+ await this.config.runHook('preupdate', { channel, version: updated });
69
+ await this.update(manifest, current, updated, force, channel);
70
+ }
71
+ await this.config.runHook('update', { channel, version: updated });
72
+ core_1.CliUx.ux.action.stop();
55
73
  }
56
- cli_ux_1.default.debug('done');
57
- cli_ux_1.default.action.stop();
74
+ await this.touch();
75
+ await this.tidy();
76
+ core_1.CliUx.ux.debug('done');
77
+ }
78
+ async findLocalVersions() {
79
+ await this.ensureClientDir();
80
+ return fs
81
+ .readdirSync(this.clientRoot)
82
+ .filter(dirOrFile => dirOrFile !== 'bin' && dirOrFile !== 'current')
83
+ .map(f => path.join(this.clientRoot, f));
58
84
  }
59
- async fetchManifest() {
60
- const http = require('http-call').HTTP;
61
- cli_ux_1.default.action.status = 'fetching manifest';
85
+ async fetchVersionIndex() {
86
+ core_1.CliUx.ux.action.status = 'fetching version index';
87
+ const newIndexUrl = this.config.s3Url(this.s3VersionIndexKey());
62
88
  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`
89
+ const { body } = await http_call_1.default.get(newIndexUrl);
71
90
  if (typeof body === 'string') {
72
91
  return JSON.parse(body);
73
92
  }
74
93
  return body;
75
94
  }
95
+ catch (_a) {
96
+ throw new Error(`No version indices exist for ${this.config.name}.`);
97
+ }
98
+ }
99
+ async ensureClientDir() {
100
+ try {
101
+ await fs.mkdirp(this.clientRoot);
102
+ }
103
+ catch (error) {
104
+ if (error.code === 'EEXIST') {
105
+ // for some reason the client directory is sometimes a file
106
+ // if so, this happens. Delete it and recreate
107
+ await fs.remove(this.clientRoot);
108
+ await fs.mkdirp(this.clientRoot);
109
+ }
110
+ else {
111
+ throw error;
112
+ }
113
+ }
114
+ }
115
+ composeS3SubDir() {
116
+ let s3SubDir = this.config.pjson.oclif.update.s3.folder || '';
117
+ if (s3SubDir !== '' && s3SubDir.slice(-1) !== '/')
118
+ s3SubDir = `${s3SubDir}/`;
119
+ return s3SubDir;
120
+ }
121
+ s3ChannelManifestKey(channel) {
122
+ const { bin, platform, arch } = this.config;
123
+ const s3SubDir = this.composeS3SubDir();
124
+ return path.join(s3SubDir, 'channels', channel, `${bin}-${platform}-${arch}-buildmanifest`);
125
+ }
126
+ s3VersionManifestKey(version, hash) {
127
+ const { bin, platform, arch } = this.config;
128
+ const s3SubDir = this.composeS3SubDir();
129
+ return path.join(s3SubDir, 'versions', version, hash, `${bin}-v${version}-${hash}-${platform}-${arch}-buildmanifest`);
130
+ }
131
+ s3VersionIndexKey() {
132
+ const { bin, platform, arch } = this.config;
133
+ const s3SubDir = this.composeS3SubDir();
134
+ return path.join(s3SubDir, 'versions', `${bin}-${platform}-${arch}-tar-gz.json`);
135
+ }
136
+ async fetchChannelManifest(channel) {
137
+ const s3Key = this.s3ChannelManifestKey(channel);
138
+ try {
139
+ return await this.fetchManifest(s3Key);
140
+ }
76
141
  catch (error) {
77
142
  if (error.statusCode === 403)
78
- throw new Error(`HTTP 403: Invalid channel ${this.channel}`);
143
+ throw new Error(`HTTP 403: Invalid channel ${channel}`);
79
144
  throw error;
80
145
  }
81
146
  }
147
+ async fetchVersionManifest(version, url) {
148
+ const parts = url.split('/');
149
+ const hashIndex = parts.indexOf(version) + 1;
150
+ const hash = parts[hashIndex];
151
+ const s3Key = this.s3VersionManifestKey(version, hash);
152
+ return this.fetchManifest(s3Key);
153
+ }
154
+ async fetchManifest(s3Key) {
155
+ core_1.CliUx.ux.action.status = 'fetching manifest';
156
+ const url = this.config.s3Url(s3Key);
157
+ const { body } = await http_call_1.default.get(url);
158
+ if (typeof body === 'string') {
159
+ return JSON.parse(body);
160
+ }
161
+ return body;
162
+ }
82
163
  async downloadAndExtract(output, manifest, channel) {
83
164
  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', {
165
+ const gzUrl = gz || this.config.s3Url(this.config.s3Key('versioned', {
90
166
  version,
91
167
  channel,
92
- bin: this.options.config.bin,
93
- platform: this.options.config.platform,
94
- arch: this.options.config.arch,
168
+ bin: this.config.bin,
169
+ platform: this.config.platform,
170
+ arch: this.config.arch,
95
171
  ext: 'gz',
96
172
  }));
97
- const { response: stream } = await http.stream(gzUrl);
173
+ const { response: stream } = await http_call_1.default.stream(gzUrl);
98
174
  stream.pause();
99
- const baseDir = manifest.baseDir || this.options.config.s3Key('baseDir', {
175
+ const baseDir = manifest.baseDir || this.config.s3Key('baseDir', {
100
176
  version,
101
177
  channel,
102
- bin: this.options.config.bin,
103
- platform: this.options.config.platform,
104
- arch: this.options.config.arch,
178
+ bin: this.config.bin,
179
+ platform: this.config.platform,
180
+ arch: this.config.arch,
105
181
  });
106
182
  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
183
+ if (core_1.CliUx.ux.action.type === 'spinner') {
110
184
  const total = Number.parseInt(stream.headers['content-length'], 10);
111
185
  let current = 0;
112
- const updateStatus = _.throttle((newStatus) => {
113
- cli_ux_1.default.action.status = newStatus;
186
+ const updateStatus = (0, lodash_throttle_1.default)((newStatus) => {
187
+ core_1.CliUx.ux.action.status = newStatus;
114
188
  }, 250, { leading: true, trailing: false });
115
189
  stream.on('data', data => {
116
190
  current += data.length;
@@ -120,73 +194,72 @@ class UpdateCli {
120
194
  stream.resume();
121
195
  await extraction;
122
196
  }
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) + ')'}`);
197
+ // eslint-disable-next-line max-params
198
+ async update(manifest, current, updated, force, channel) {
199
+ core_1.CliUx.ux.action.start(`${this.config.name}: Updating CLI from ${color_1.default.green(current)} to ${color_1.default.green(updated)}${channel === 'stable' ? '' : ' (' + color_1.default.yellow(channel) + ')'}`);
125
200
  await this.ensureClientDir();
126
- const output = path.join(this.clientRoot, this.updatedVersion);
127
- if (!await fs.pathExists(output)) {
201
+ const output = path.join(this.clientRoot, updated);
202
+ if (force || !await fs.pathExists(output))
128
203
  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();
204
+ await this.refreshConfig(updated);
205
+ await this.setChannel(channel);
206
+ await this.createBin(updated);
134
207
  }
135
- async updateToExistingVersion(version) {
136
- await this.createBin(version);
137
- await this.touch();
208
+ async updateToExistingVersion(current, updated) {
209
+ core_1.CliUx.ux.action.start(`${this.config.name}: Updating CLI from ${color_1.default.green(current)} to ${color_1.default.green(updated)}`);
210
+ await this.ensureClientDir();
211
+ await this.refreshConfig(updated);
212
+ await this.createBin(updated);
138
213
  }
139
- async skipUpdate() {
140
- if (!this.options.config.binPath) {
141
- const instructions = this.options.config.scopedEnvVar('UPDATE_INSTRUCTIONS');
214
+ notUpdatable() {
215
+ if (!this.config.binPath) {
216
+ const instructions = this.config.scopedEnvVar('UPDATE_INSTRUCTIONS');
142
217
  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}`;
218
+ core_1.CliUx.ux.warn(instructions);
219
+ return true;
150
220
  }
151
221
  return false;
152
222
  }
223
+ alreadyOnVersion(current, updated) {
224
+ return current === updated;
225
+ }
153
226
  async determineChannel() {
154
- const channelPath = path.join(this.options.config.dataDir, 'channel');
227
+ const channelPath = path.join(this.config.dataDir, 'channel');
155
228
  if (fs.existsSync(channelPath)) {
156
229
  const channel = await fs.readFile(channelPath, 'utf8');
157
230
  return String(channel).trim();
158
231
  }
159
- return this.options.config.channel || 'stable';
232
+ return this.config.channel || 'stable';
160
233
  }
161
234
  async determineCurrentVersion() {
162
235
  try {
163
236
  const currentVersion = await fs.readFile(this.clientBin, 'utf8');
164
237
  const matches = currentVersion.match(/\.\.[/\\|](.+)[/\\|]bin/);
165
- return matches ? matches[1] : this.options.config.version;
238
+ return matches ? matches[1] : this.config.version;
166
239
  }
167
240
  catch (error) {
168
- cli_ux_1.default.debug(error);
241
+ core_1.CliUx.ux.debug(error);
169
242
  }
170
- return this.options.config.version;
243
+ return this.config.version;
171
244
  }
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`);
245
+ async findLocalVersion(version) {
246
+ const versions = await this.findLocalVersions();
247
+ return versions
248
+ .map(file => path.basename(file))
249
+ .find(file => file.startsWith(version));
177
250
  }
178
- async setChannel() {
179
- const channelPath = path.join(this.options.config.dataDir, 'channel');
180
- fs.writeFile(channelPath, this.channel, 'utf8');
251
+ async setChannel(channel) {
252
+ const channelPath = path.join(this.config.dataDir, 'channel');
253
+ fs.writeFile(channelPath, channel, 'utf8');
181
254
  }
182
255
  async logChop() {
183
256
  try {
184
- cli_ux_1.default.debug('log chop');
257
+ core_1.CliUx.ux.debug('log chop');
185
258
  const logChopper = require('log-chopper').default;
186
- await logChopper.chop(this.options.config.errlog);
259
+ await logChopper.chop(this.config.errlog);
187
260
  }
188
261
  catch (error) {
189
- cli_ux_1.default.debug(error.message);
262
+ core_1.CliUx.ux.debug(error.message);
190
263
  }
191
264
  }
192
265
  async mtime(f) {
@@ -196,32 +269,33 @@ class UpdateCli {
196
269
  // when autoupdating, wait until the CLI isn't active
197
270
  async debounce() {
198
271
  let output = false;
199
- const lastrunfile = path.join(this.options.config.cacheDir, 'lastrun');
272
+ const lastrunfile = path.join(this.config.cacheDir, 'lastrun');
200
273
  const m = await this.mtime(lastrunfile);
201
274
  m.setHours(m.getHours() + 1);
202
275
  if (m > new Date()) {
203
276
  const msg = `waiting until ${m.toISOString()} to update`;
204
277
  if (output) {
205
- cli_ux_1.default.debug(msg);
278
+ core_1.CliUx.ux.debug(msg);
206
279
  }
207
280
  else {
208
- await cli_ux_1.default.log(msg);
281
+ core_1.CliUx.ux.log(msg);
209
282
  output = true;
210
283
  }
211
284
  await (0, util_1.wait)(60 * 1000); // wait 1 minute
212
285
  return this.debounce();
213
286
  }
214
- cli_ux_1.default.log('time to update');
287
+ core_1.CliUx.ux.log('time to update');
215
288
  }
216
289
  // removes any unused CLIs
217
290
  async tidy() {
291
+ core_1.CliUx.ux.debug('tidy');
218
292
  try {
219
293
  const root = this.clientRoot;
220
294
  if (!await fs.pathExists(root))
221
295
  return;
222
296
  const files = await (0, util_1.ls)(root);
223
297
  const promises = files.map(async (f) => {
224
- if (['bin', 'current', this.options.config.version].includes(path.basename(f.path)))
298
+ if (['bin', 'current', this.config.version].includes(path.basename(f.path)))
225
299
  return;
226
300
  const mtime = f.stat.mtime;
227
301
  mtime.setHours(mtime.getHours() + (42 * 24));
@@ -234,47 +308,30 @@ class UpdateCli {
234
308
  await this.logChop();
235
309
  }
236
310
  catch (error) {
237
- cli_ux_1.default.warn(error);
311
+ core_1.CliUx.ux.warn(error);
238
312
  }
239
313
  }
240
314
  async touch() {
241
315
  // touch the client so it won't be tidied up right away
242
316
  try {
243
- const p = path.join(this.clientRoot, this.options.config.version);
244
- cli_ux_1.default.debug('touching client at', p);
317
+ const p = path.join(this.clientRoot, this.config.version);
318
+ core_1.CliUx.ux.debug('touching client at', p);
245
319
  if (!await fs.pathExists(p))
246
320
  return;
247
321
  await fs.utimes(p, new Date(), new Date());
248
322
  }
249
323
  catch (error) {
250
- cli_ux_1.default.warn(error);
324
+ core_1.CliUx.ux.warn(error);
251
325
  }
252
326
  }
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
- });
327
+ async refreshConfig(version) {
328
+ this.config = await core_1.Config.load({ root: path.join(this.clientRoot, version) });
272
329
  }
273
330
  async createBin(version) {
274
331
  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');
332
+ const { bin, windows } = this.config;
333
+ const binPathEnvVar = this.config.scopedEnvVarKey('BINPATH');
334
+ const redirectedEnvVar = this.config.scopedEnvVarKey('REDIRECTED');
278
335
  if (windows) {
279
336
  const body = `@echo off
280
337
  setlocal enableextensions
@@ -311,21 +368,5 @@ ${binPathEnvVar}="\$DIR/${bin}" ${redirectedEnvVar}=1 "$DIR/../${version}/bin/${
311
368
  await fs.symlink(`./${version}`, path.join(this.clientRoot, 'current'));
312
369
  }
313
370
  }
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
371
  }
331
- exports.default = UpdateCli;
372
+ exports.Updater = Updater;
package/lib/util.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  /// <reference types="node" />
2
2
  import * as fs from 'fs-extra';
3
3
  export declare function touch(p: string): Promise<void>;
4
- export declare function ls(dir: string): Promise<{
4
+ export declare function ls(dir: string): Promise<Array<{
5
5
  path: string;
6
6
  stat: fs.Stats;
7
- }[]>;
7
+ }>>;
8
+ export declare function rm(dir: string): Promise<void>;
8
9
  export declare function wait(ms: number, unref?: boolean): Promise<void>;
package/lib/util.js CHANGED
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.wait = exports.ls = exports.touch = void 0;
4
- const fs = require("fs-extra");
5
- const path = require("path");
3
+ exports.wait = exports.rm = exports.ls = exports.touch = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const fs = (0, tslib_1.__importStar)(require("fs-extra"));
6
+ const path = (0, tslib_1.__importStar)(require("path"));
6
7
  async function touch(p) {
7
8
  try {
8
9
  await fs.utimes(p, new Date(), new Date());
@@ -18,6 +19,14 @@ async function ls(dir) {
18
19
  return Promise.all(paths.map(path => fs.stat(path).then(stat => ({ path, stat }))));
19
20
  }
20
21
  exports.ls = ls;
22
+ async function rm(dir) {
23
+ return new Promise(resolve => {
24
+ fs.rm(dir, { recursive: true, force: true }, () => {
25
+ resolve();
26
+ });
27
+ });
28
+ }
29
+ exports.rm = rm;
21
30
  function wait(ms, unref = false) {
22
31
  return new Promise(resolve => {
23
32
  const t = setTimeout(() => resolve(), ms);
@@ -1 +1 @@
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"}]}}}
1
+ {"version":"3.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":[],"examples":[{"description":"Update to the stable channel:","command":"<%= config.bin %> <%= command.id %> stable"},{"description":"Update to a specific version:","command":"<%= config.bin %> <%= command.id %> --version 1.0.0"},{"description":"Interactively select version:","command":"<%= config.bin %> <%= command.id %> --interactive"},{"description":"See available versions:","command":"<%= config.bin %> <%= command.id %> --available"}],"flags":{"autoupdate":{"name":"autoupdate","type":"boolean","hidden":true,"allowNo":false},"available":{"name":"available","type":"boolean","char":"a","description":"Install a specific version.","allowNo":false},"version":{"name":"version","type":"option","char":"v","description":"Install a specific version.","multiple":false,"exclusive":["interactive"]},"interactive":{"name":"interactive","type":"boolean","char":"i","description":"Interactively select version to install. This is ignored if a channel is provided.","allowNo":false,"exclusive":["version"]},"force":{"name":"force","type":"boolean","description":"Force a re-download of the requested version.","allowNo":false}},"args":[{"name":"channel"}]}}}
package/package.json CHANGED
@@ -1,34 +1,35 @@
1
1
  {
2
2
  "name": "@oclif/plugin-update",
3
- "version": "2.1.3",
3
+ "version": "3.0.0",
4
4
  "author": "Salesforce",
5
5
  "bugs": "https://github.com/oclif/plugin-update/issues",
6
6
  "dependencies": {
7
7
  "@oclif/color": "^1.0.0",
8
- "@oclif/core": "1.0.10",
9
- "@types/semver": "^7.3.4",
10
- "cli-ux": "6.0.6",
8
+ "@oclif/core": "^1.3.0",
11
9
  "cross-spawn": "^7.0.3",
12
10
  "debug": "^4.3.1",
13
11
  "filesize": "^6.1.0",
14
12
  "fs-extra": "^9.0.1",
15
13
  "http-call": "^5.3.0",
16
- "lodash": "^4.17.21",
14
+ "inquirer": "^8.2.0",
15
+ "lodash.throttle": "^4.1.1",
17
16
  "log-chopper": "^1.0.2",
18
17
  "semver": "^7.3.5",
19
18
  "tar-fs": "^2.1.1"
20
19
  },
21
20
  "devDependencies": {
22
- "@oclif/plugin-help": "5.1.9",
21
+ "@oclif/plugin-help": "^5.1.9",
23
22
  "@oclif/test": "^2.0.2",
24
23
  "@types/chai": "^4.2.15",
25
24
  "@types/cross-spawn": "^6.0.2",
26
25
  "@types/execa": "^0.9.0",
27
26
  "@types/fs-extra": "^8.0.1",
28
27
  "@types/glob": "^7.1.3",
29
- "@types/lodash": "^4.14.168",
28
+ "@types/inquirer": "^8.2.0",
29
+ "@types/lodash.throttle": "^4.1.6",
30
30
  "@types/mocha": "^9",
31
31
  "@types/node": "^14.14.31",
32
+ "@types/semver": "^7.3.4",
32
33
  "@types/supports-color": "^7.2.0",
33
34
  "@types/write-json-file": "^3.2.1",
34
35
  "chai": "^4.3.4",
@@ -38,7 +39,7 @@
38
39
  "globby": "^11.0.2",
39
40
  "mocha": "^9",
40
41
  "nock": "^13.2.1",
41
- "oclif": "2.0.0-main.19",
42
+ "oclif": "^2.4.2",
42
43
  "qqjs": "^0.3.11",
43
44
  "sinon": "^12.0.1",
44
45
  "ts-node": "^9.1.1",
@@ -78,5 +79,6 @@
78
79
  "postpack": "rm -f oclif.manifest.json",
79
80
  "version": "oclif readme && git add README.md",
80
81
  "build": "rm -rf lib && tsc"
81
- }
82
+ },
83
+ "main": "lib/index.js"
82
84
  }