@oclif/plugin-update 2.2.0 → 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,40 @@
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
+
5
39
  ## [2.2.0](https://github.com/oclif/plugin-update/compare/v2.1.5...v2.2.0) (2022-01-28)
6
40
 
7
41
 
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.5/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,27 +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 path = require("path");
5
+ const inquirer_1 = require("inquirer");
6
+ const path = (0, tslib_1.__importStar)(require("path"));
7
+ const semver_1 = require("semver");
5
8
  const update_1 = require("../update");
6
- async function getPinToVersion() {
7
- return core_1.CliUx.ux.prompt('Enter a version to update to');
8
- }
9
9
  class UpdateCommand extends core_1.Command {
10
- constructor() {
11
- super(...arguments);
12
- this.clientRoot = this.config.scopedEnvVar('OCLIF_CLIENT_HOME') || path.join(this.config.dataDir, 'client');
13
- this.clientBin = path.join(this.clientRoot, 'bin', this.config.windows ? `${this.config.bin}.cmd` : this.config.bin);
14
- }
15
10
  async run() {
16
11
  const { args, flags } = await this.parse(UpdateCommand);
17
- const updateCli = new update_1.default({ channel: args.channel, autoUpdate: flags.autoupdate, fromLocal: flags['from-local'], config: this.config, exit: this.exit, getPinToVersion: getPinToVersion });
18
- 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;
19
43
  }
20
44
  }
21
45
  exports.default = UpdateCommand;
22
46
  UpdateCommand.description = 'update the <%= config.bin %> CLI';
23
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
+ ];
24
66
  UpdateCommand.flags = {
25
67
  autoupdate: core_1.Flags.boolean({ hidden: true }),
26
- '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
+ }),
27
85
  };
package/lib/hooks/init.js CHANGED
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.init = void 0;
4
+ const tslib_1 = require("tslib");
4
5
  const core_1 = require("@oclif/core");
5
- const spawn = require("cross-spawn");
6
- const fs = require("fs-extra");
7
- const path = require("path");
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");
9
10
  // eslint-disable-next-line unicorn/prefer-module
10
11
  const debug = require('debug')('cli:updater');
@@ -55,7 +56,7 @@ const init = async function (opts) {
55
56
  debug(`spawning autoupdate on ${binPath}`);
56
57
  const fd = await fs.open(autoupdatelogfile, 'a');
57
58
  fs.write(fd, timestamp(`starting \`${binPath} update --autoupdate\` from ${process.argv.slice(1, 3).join(' ')}\n`));
58
- spawn(binPath, ['update', '--autoupdate'], {
59
+ (0, cross_spawn_1.default)(binPath, ['update', '--autoupdate'], {
59
60
  detached: !this.config.windows,
60
61
  stdio: ['ignore', fd, fd],
61
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,189 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Updater = void 0;
4
+ const tslib_1 = require("tslib");
3
5
  /* eslint-disable unicorn/prefer-module */
4
- const color_1 = require("@oclif/color");
6
+ const color_1 = (0, tslib_1.__importDefault)(require("@oclif/color"));
5
7
  const core_1 = require("@oclif/core");
6
- const spawn = require("cross-spawn");
7
- const fs = require("fs-extra");
8
- const _ = require("lodash");
9
- const path = require("path");
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"));
10
13
  const tar_1 = require("./tar");
11
14
  const util_1 = require("./util");
12
- class UpdateCli {
13
- constructor(options) {
14
- this.options = options;
15
- this.clientRoot = this.options.config.scopedEnvVar('OCLIF_CLIENT_HOME') || path.join(this.options.config.dataDir, 'client');
16
- 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);
17
24
  }
18
- async runUpdate() {
19
- if (this.options.autoUpdate)
25
+ async runUpdate(options) {
26
+ const { autoUpdate, version, force = false } = options;
27
+ if (autoUpdate)
20
28
  await this.debounce();
21
- this.channel = this.options.channel || await this.determineChannel();
22
- if (this.options.fromLocal) {
23
- await this.ensureClientDir();
24
- core_1.CliUx.ux.debug(`Looking for locally installed versions at ${this.clientRoot}`);
25
- // Do not show known non-local version folder names, bin and current.
26
- const versions = fs.readdirSync(this.clientRoot).filter(dirOrFile => dirOrFile !== 'bin' && dirOrFile !== 'current');
27
- if (versions.length === 0)
28
- throw new Error('No locally installed versions found.');
29
- core_1.CliUx.ux.log(`Found versions: \n${versions.map(version => ` ${version}`).join('\n')}\n`);
30
- const pinToVersion = await this.options.getPinToVersion();
31
- if (!versions.includes(pinToVersion))
32
- throw new Error(`Version ${pinToVersion} not found in the locally installed versions.`);
33
- if (!await fs.pathExists(path.join(this.clientRoot, pinToVersion))) {
34
- 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);
35
45
  }
36
- core_1.CliUx.ux.action.start(`${this.options.config.name}: Updating CLI`);
37
- core_1.CliUx.ux.debug(`switching to existing version ${pinToVersion}`);
38
- this.updateToExistingVersion(pinToVersion);
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();
39
58
  core_1.CliUx.ux.log();
40
- core_1.CliUx.ux.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}.`);
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}.`);
41
60
  }
42
61
  else {
43
- core_1.CliUx.ux.action.start(`${this.options.config.name}: Updating CLI`);
44
- await this.options.config.runHook('preupdate', { channel: this.channel });
45
- const manifest = await this.fetchManifest();
46
- this.currentVersion = await this.determineCurrentVersion();
47
- this.updatedVersion = manifest.sha ? `${manifest.version}-${manifest.sha}` : manifest.version;
48
- const reason = await this.skipUpdate();
49
- if (reason)
50
- core_1.CliUx.ux.action.stop(reason || 'done');
51
- else
52
- await this.update(manifest);
53
- core_1.CliUx.ux.debug('tidy');
54
- await this.tidy();
55
- 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();
56
73
  }
74
+ await this.touch();
75
+ await this.tidy();
57
76
  core_1.CliUx.ux.debug('done');
58
- core_1.CliUx.ux.action.stop();
59
77
  }
60
- async fetchManifest() {
61
- const http = require('http-call').HTTP;
62
- core_1.CliUx.ux.action.status = 'fetching manifest';
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));
84
+ }
85
+ async fetchVersionIndex() {
86
+ core_1.CliUx.ux.action.status = 'fetching version index';
87
+ const newIndexUrl = this.config.s3Url(this.s3VersionIndexKey());
63
88
  try {
64
- const url = this.options.config.s3Url(this.options.config.s3Key('manifest', {
65
- channel: this.channel,
66
- platform: this.options.config.platform,
67
- arch: this.options.config.arch,
68
- }));
69
- const { body } = await http.get(url);
70
- // in case the content-type is not set, parse as a string
71
- // this will happen if uploading without `oclif-dev publish`
89
+ const { body } = await http_call_1.default.get(newIndexUrl);
72
90
  if (typeof body === 'string') {
73
91
  return JSON.parse(body);
74
92
  }
75
93
  return body;
76
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
+ }
77
141
  catch (error) {
78
142
  if (error.statusCode === 403)
79
- throw new Error(`HTTP 403: Invalid channel ${this.channel}`);
143
+ throw new Error(`HTTP 403: Invalid channel ${channel}`);
80
144
  throw error;
81
145
  }
82
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
+ }
83
163
  async downloadAndExtract(output, manifest, channel) {
84
164
  const { version, gz, sha256gz } = manifest;
85
- const filesize = (n) => {
86
- const [num, suffix] = require('filesize')(n, { output: 'array' });
87
- return num.toFixed(1) + ` ${suffix}`;
88
- };
89
- const http = require('http-call').HTTP;
90
- const gzUrl = gz || this.options.config.s3Url(this.options.config.s3Key('versioned', {
165
+ const gzUrl = gz || this.config.s3Url(this.config.s3Key('versioned', {
91
166
  version,
92
167
  channel,
93
- bin: this.options.config.bin,
94
- platform: this.options.config.platform,
95
- arch: this.options.config.arch,
168
+ bin: this.config.bin,
169
+ platform: this.config.platform,
170
+ arch: this.config.arch,
96
171
  ext: 'gz',
97
172
  }));
98
- const { response: stream } = await http.stream(gzUrl);
173
+ const { response: stream } = await http_call_1.default.stream(gzUrl);
99
174
  stream.pause();
100
- const baseDir = manifest.baseDir || this.options.config.s3Key('baseDir', {
175
+ const baseDir = manifest.baseDir || this.config.s3Key('baseDir', {
101
176
  version,
102
177
  channel,
103
- bin: this.options.config.bin,
104
- platform: this.options.config.platform,
105
- arch: this.options.config.arch,
178
+ bin: this.config.bin,
179
+ platform: this.config.platform,
180
+ arch: this.config.arch,
106
181
  });
107
182
  const extraction = (0, tar_1.extract)(stream, baseDir, output, sha256gz);
108
- // to-do: use cli.action.type
109
- if (core_1.CliUx.ux.action.frames) {
110
- // if spinner action
183
+ if (core_1.CliUx.ux.action.type === 'spinner') {
111
184
  const total = Number.parseInt(stream.headers['content-length'], 10);
112
185
  let current = 0;
113
- const updateStatus = _.throttle((newStatus) => {
186
+ const updateStatus = (0, lodash_throttle_1.default)((newStatus) => {
114
187
  core_1.CliUx.ux.action.status = newStatus;
115
188
  }, 250, { leading: true, trailing: false });
116
189
  stream.on('data', data => {
@@ -121,70 +194,69 @@ class UpdateCli {
121
194
  stream.resume();
122
195
  await extraction;
123
196
  }
124
- async update(manifest, channel = 'stable') {
125
- core_1.CliUx.ux.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) + ')'}`);
126
200
  await this.ensureClientDir();
127
- const output = path.join(this.clientRoot, this.updatedVersion);
128
- if (!await fs.pathExists(output)) {
201
+ const output = path.join(this.clientRoot, updated);
202
+ if (force || !await fs.pathExists(output))
129
203
  await this.downloadAndExtract(output, manifest, channel);
130
- }
131
- await this.setChannel();
132
- await this.createBin(this.updatedVersion);
133
- await this.touch();
134
- await this.reexec();
204
+ await this.refreshConfig(updated);
205
+ await this.setChannel(channel);
206
+ await this.createBin(updated);
135
207
  }
136
- async updateToExistingVersion(version) {
137
- await this.createBin(version);
138
- 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);
139
213
  }
140
- async skipUpdate() {
141
- if (!this.options.config.binPath) {
142
- const instructions = this.options.config.scopedEnvVar('UPDATE_INSTRUCTIONS');
214
+ notUpdatable() {
215
+ if (!this.config.binPath) {
216
+ const instructions = this.config.scopedEnvVar('UPDATE_INSTRUCTIONS');
143
217
  if (instructions)
144
218
  core_1.CliUx.ux.warn(instructions);
145
- return 'not updatable';
146
- }
147
- if (this.currentVersion === this.updatedVersion) {
148
- if (this.options.config.scopedEnvVar('HIDE_UPDATED_MESSAGE'))
149
- return 'done';
150
- return `already on latest version: ${this.currentVersion}`;
219
+ return true;
151
220
  }
152
221
  return false;
153
222
  }
223
+ alreadyOnVersion(current, updated) {
224
+ return current === updated;
225
+ }
154
226
  async determineChannel() {
155
- const channelPath = path.join(this.options.config.dataDir, 'channel');
227
+ const channelPath = path.join(this.config.dataDir, 'channel');
156
228
  if (fs.existsSync(channelPath)) {
157
229
  const channel = await fs.readFile(channelPath, 'utf8');
158
230
  return String(channel).trim();
159
231
  }
160
- return this.options.config.channel || 'stable';
232
+ return this.config.channel || 'stable';
161
233
  }
162
234
  async determineCurrentVersion() {
163
235
  try {
164
236
  const currentVersion = await fs.readFile(this.clientBin, 'utf8');
165
237
  const matches = currentVersion.match(/\.\.[/\\|](.+)[/\\|]bin/);
166
- return matches ? matches[1] : this.options.config.version;
238
+ return matches ? matches[1] : this.config.version;
167
239
  }
168
240
  catch (error) {
169
241
  core_1.CliUx.ux.debug(error);
170
242
  }
171
- return this.options.config.version;
243
+ return this.config.version;
172
244
  }
173
- s3ChannelManifestKey(bin, platform, arch, folder = '') {
174
- let s3SubDir = folder || '';
175
- if (s3SubDir !== '' && s3SubDir.slice(-1) !== '/')
176
- s3SubDir = `${s3SubDir}/`;
177
- 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));
178
250
  }
179
- async setChannel() {
180
- const channelPath = path.join(this.options.config.dataDir, 'channel');
181
- 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');
182
254
  }
183
255
  async logChop() {
184
256
  try {
185
257
  core_1.CliUx.ux.debug('log chop');
186
258
  const logChopper = require('log-chopper').default;
187
- await logChopper.chop(this.options.config.errlog);
259
+ await logChopper.chop(this.config.errlog);
188
260
  }
189
261
  catch (error) {
190
262
  core_1.CliUx.ux.debug(error.message);
@@ -197,7 +269,7 @@ class UpdateCli {
197
269
  // when autoupdating, wait until the CLI isn't active
198
270
  async debounce() {
199
271
  let output = false;
200
- const lastrunfile = path.join(this.options.config.cacheDir, 'lastrun');
272
+ const lastrunfile = path.join(this.config.cacheDir, 'lastrun');
201
273
  const m = await this.mtime(lastrunfile);
202
274
  m.setHours(m.getHours() + 1);
203
275
  if (m > new Date()) {
@@ -206,7 +278,7 @@ class UpdateCli {
206
278
  core_1.CliUx.ux.debug(msg);
207
279
  }
208
280
  else {
209
- await core_1.CliUx.ux.log(msg);
281
+ core_1.CliUx.ux.log(msg);
210
282
  output = true;
211
283
  }
212
284
  await (0, util_1.wait)(60 * 1000); // wait 1 minute
@@ -216,13 +288,14 @@ class UpdateCli {
216
288
  }
217
289
  // removes any unused CLIs
218
290
  async tidy() {
291
+ core_1.CliUx.ux.debug('tidy');
219
292
  try {
220
293
  const root = this.clientRoot;
221
294
  if (!await fs.pathExists(root))
222
295
  return;
223
296
  const files = await (0, util_1.ls)(root);
224
297
  const promises = files.map(async (f) => {
225
- if (['bin', 'current', this.options.config.version].includes(path.basename(f.path)))
298
+ if (['bin', 'current', this.config.version].includes(path.basename(f.path)))
226
299
  return;
227
300
  const mtime = f.stat.mtime;
228
301
  mtime.setHours(mtime.getHours() + (42 * 24));
@@ -241,7 +314,7 @@ class UpdateCli {
241
314
  async touch() {
242
315
  // touch the client so it won't be tidied up right away
243
316
  try {
244
- const p = path.join(this.clientRoot, this.options.config.version);
317
+ const p = path.join(this.clientRoot, this.config.version);
245
318
  core_1.CliUx.ux.debug('touching client at', p);
246
319
  if (!await fs.pathExists(p))
247
320
  return;
@@ -251,31 +324,14 @@ class UpdateCli {
251
324
  core_1.CliUx.ux.warn(error);
252
325
  }
253
326
  }
254
- async reexec() {
255
- core_1.CliUx.ux.action.stop();
256
- return new Promise((_, reject) => {
257
- core_1.CliUx.ux.debug('restarting CLI after update', this.clientBin);
258
- spawn(this.clientBin, ['update'], {
259
- stdio: 'inherit',
260
- env: Object.assign(Object.assign({}, process.env), { [this.options.config.scopedEnvVarKey('HIDE_UPDATED_MESSAGE')]: '1' }),
261
- })
262
- .on('error', reject)
263
- .on('close', (status) => {
264
- try {
265
- if (status > 0)
266
- this.options.exit(status);
267
- }
268
- catch (error) {
269
- reject(error);
270
- }
271
- });
272
- });
327
+ async refreshConfig(version) {
328
+ this.config = await core_1.Config.load({ root: path.join(this.clientRoot, version) });
273
329
  }
274
330
  async createBin(version) {
275
331
  const dst = this.clientBin;
276
- const { bin, windows } = this.options.config;
277
- const binPathEnvVar = this.options.config.scopedEnvVarKey('BINPATH');
278
- 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');
279
335
  if (windows) {
280
336
  const body = `@echo off
281
337
  setlocal enableextensions
@@ -312,21 +368,5 @@ ${binPathEnvVar}="\$DIR/${bin}" ${redirectedEnvVar}=1 "$DIR/../${version}/bin/${
312
368
  await fs.symlink(`./${version}`, path.join(this.clientRoot, 'current'));
313
369
  }
314
370
  }
315
- async ensureClientDir() {
316
- try {
317
- await fs.mkdirp(this.clientRoot);
318
- }
319
- catch (error) {
320
- if (error.code === 'EEXIST') {
321
- // for some reason the client directory is sometimes a file
322
- // if so, this happens. Delete it and recreate
323
- await fs.remove(this.clientRoot);
324
- await fs.mkdirp(this.clientRoot);
325
- }
326
- else {
327
- throw error;
328
- }
329
- }
330
- }
331
371
  }
332
- 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.2.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":"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,18 +1,18 @@
1
1
  {
2
2
  "name": "@oclif/plugin-update",
3
- "version": "2.2.0",
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.2.0",
9
- "@types/semver": "^7.3.4",
8
+ "@oclif/core": "^1.3.0",
10
9
  "cross-spawn": "^7.0.3",
11
10
  "debug": "^4.3.1",
12
11
  "filesize": "^6.1.0",
13
12
  "fs-extra": "^9.0.1",
14
13
  "http-call": "^5.3.0",
15
- "lodash": "^4.17.21",
14
+ "inquirer": "^8.2.0",
15
+ "lodash.throttle": "^4.1.1",
16
16
  "log-chopper": "^1.0.2",
17
17
  "semver": "^7.3.5",
18
18
  "tar-fs": "^2.1.1"
@@ -25,9 +25,11 @@
25
25
  "@types/execa": "^0.9.0",
26
26
  "@types/fs-extra": "^8.0.1",
27
27
  "@types/glob": "^7.1.3",
28
- "@types/lodash": "^4.14.168",
28
+ "@types/inquirer": "^8.2.0",
29
+ "@types/lodash.throttle": "^4.1.6",
29
30
  "@types/mocha": "^9",
30
31
  "@types/node": "^14.14.31",
32
+ "@types/semver": "^7.3.4",
31
33
  "@types/supports-color": "^7.2.0",
32
34
  "@types/write-json-file": "^3.2.1",
33
35
  "chai": "^4.3.4",
@@ -37,7 +39,7 @@
37
39
  "globby": "^11.0.2",
38
40
  "mocha": "^9",
39
41
  "nock": "^13.2.1",
40
- "oclif": "^2.3.0",
42
+ "oclif": "^2.4.2",
41
43
  "qqjs": "^0.3.11",
42
44
  "sinon": "^12.0.1",
43
45
  "ts-node": "^9.1.1",