From c8e10456a4f3b0fc4f94cfea5df4ae546aed3d0b Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Mon, 31 Jan 2022 20:01:53 -0700 Subject: [PATCH 01/17] feat: add --version to update command --- src/commands/update.ts | 25 ++++++++--- src/update.ts | 98 +++++++++++++++++++++++++++++++++++++----- test/update.test.ts | 1 + 3 files changed, 107 insertions(+), 17 deletions(-) diff --git a/src/commands/update.ts b/src/commands/update.ts index 93e708b3..eda699dc 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -13,18 +13,31 @@ export default class UpdateCommand extends Command { static flags = { autoupdate: Flags.boolean({hidden: true}), - 'from-local': Flags.boolean({description: 'interactively choose an already installed version'}), + 'from-local': Flags.boolean({description: 'Interactively choose an already installed version.'}), + version: Flags.string({ + description: 'Install a specific version.', + exclusive: ['from-local'], + }), } - private channel!: string - private readonly clientRoot = this.config.scopedEnvVar('OCLIF_CLIENT_HOME') || path.join(this.config.dataDir, 'client') - private readonly clientBin = path.join(this.clientRoot, 'bin', this.config.windows ? `${this.config.bin}.cmd` : this.config.bin) - async run(): Promise { const {args, flags} = await this.parse(UpdateCommand) - const updateCli = new UpdateCli({channel: args.channel, autoUpdate: flags.autoupdate, fromLocal: flags['from-local'], config: this.config as Config, exit: this.exit, getPinToVersion: getPinToVersion}) + + if (args.channel && flags.version) { + this.error('You cannot specifiy both a version and a channel.') + } + + const updateCli = new UpdateCli({ + channel: args.channel, + autoUpdate: flags.autoupdate, + fromLocal: flags['from-local'], + version: flags.version, + config: this.config as Config, + exit: this.exit, + getPinToVersion: getPinToVersion, + }) return updateCli.runUpdate() } } diff --git a/src/update.ts b/src/update.ts index 558a8f92..e80119e7 100644 --- a/src/update.ts +++ b/src/update.ts @@ -16,11 +16,14 @@ export interface UpdateCliOptions { channel?: string; autoUpdate: boolean; fromLocal: boolean; + version: string | undefined; config: Config; exit: any; getPinToVersion: () => Promise; } +export type VersionIndex = Record + export default class UpdateCli { private channel!: string @@ -65,12 +68,32 @@ export default class UpdateCli { CliUx.ux.log() 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}.`) + } else if (this.options.version) { + CliUx.ux.action.start(`${this.options.config.name}: Updating CLI`) + await this.options.config.runHook('preupdate', {channel: this.channel}) + + const index = await this.fetchVersionIndex() + const url = index[this.options.version] + if (!url) { + throw new Error(`${this.options.version} not found in index:\n${Object.keys(index).join(', ')}`) + } + + const manifest = await this.fetchVersionManifest(this.options.version, url) + this.currentVersion = await this.determineCurrentVersion() + this.updatedVersion = manifest.sha ? `${manifest.version}-${manifest.sha}` : manifest.version + const reason = await this.skipUpdate() + if (reason) CliUx.ux.action.stop(reason || 'done') + else await this.update(manifest) + + CliUx.ux.debug('tidy') + await this.tidy() + await this.options.config.runHook('update', {channel: this.channel}) } else { CliUx.ux.action.start(`${this.options.config.name}: Updating CLI`) await this.options.config.runHook('preupdate', {channel: this.channel}) - const manifest = await this.fetchManifest() + const manifest = await this.fetchChannelManifest() this.currentVersion = await this.determineCurrentVersion() - this.updatedVersion = (manifest as any).sha ? `${manifest.version}-${(manifest as any).sha}` : manifest.version + this.updatedVersion = manifest.sha ? `${manifest.version}-${manifest.sha}` : manifest.version const reason = await this.skipUpdate() if (reason) CliUx.ux.action.stop(reason || 'done') else await this.update(manifest) @@ -83,21 +106,39 @@ export default class UpdateCli { CliUx.ux.action.stop() } - private async fetchManifest(): Promise { + private async fetchChannelManifest(): Promise { + const s3Key = this.s3ChannelManifestKey( + this.options.config.bin, + this.options.config.platform, + this.options.config.arch, + (this.options.config.pjson.oclif.update.s3 as any).folder, + ) + return this.fetchManifest(s3Key) + } + + private async fetchVersionManifest(version: string, url: string): Promise { + const parts = url.split('/') + const hashIndex = parts.indexOf(version) + 1 + const hash = parts[hashIndex] + const s3Key = this.s3VersionManifestKey( + this.options.config.bin, + version, + hash, + this.options.config.platform, + this.options.config.arch, + (this.options.config.pjson.oclif.update.s3 as any).folder, + ) + return this.fetchManifest(s3Key) + } + + private async fetchManifest(s3Key: string): Promise { const http: typeof HTTP = require('http-call').HTTP CliUx.ux.action.status = 'fetching manifest' try { - const url = this.options.config.s3Url(this.options.config.s3Key('manifest', { - channel: this.channel, - platform: this.options.config.platform, - arch: this.options.config.arch, - })) + const url = this.options.config.s3Url(s3Key) const {body} = await http.get(url) - - // in case the content-type is not set, parse as a string - // this will happen if uploading without `oclif-dev publish` if (typeof body === 'string') { return JSON.parse(body) } @@ -109,6 +150,28 @@ export default class UpdateCli { } } + private async fetchVersionIndex(): Promise { + const http: typeof HTTP = require('http-call').HTTP + + CliUx.ux.action.status = 'fetching version index' + + const newIndexUrl = this.options.config.s3Url( + this.s3VersionIndexKey( + this.options.config.bin, + this.options.config.platform, + this.options.config.arch, + (this.options.config.pjson.oclif.update.s3 as any).folder, + ), + ) + + const {body} = await http.get(newIndexUrl) + if (typeof body === 'string') { + return JSON.parse(body) + } + + return body + } + private async downloadAndExtract(output: string, manifest: IManifest, channel: string) { const {version, gz, sha256gz} = manifest @@ -224,6 +287,19 @@ export default class UpdateCli { return path.join(s3SubDir, 'channels', this.channel, `${bin}-${platform}-${arch}-buildmanifest`) } + // eslint-disable-next-line max-params + private s3VersionManifestKey(bin: string, version: string, hash: string, platform: string, arch: string, folder = ''): string { + let s3SubDir = folder || '' + if (s3SubDir !== '' && s3SubDir.slice(-1) !== '/') s3SubDir = `${s3SubDir}/` + return path.join(s3SubDir, 'versions', version, hash, `${bin}-v${version}-${hash}-${platform}-${arch}-buildmanifest`) + } + + private s3VersionIndexKey(bin: string, platform: string, arch: string, folder = ''): string { + let s3SubDir = folder || '' + if (s3SubDir !== '' && s3SubDir.slice(-1) !== '/') s3SubDir = `${s3SubDir}/` + return path.join(s3SubDir, 'versions', `${bin}-${platform}-${arch}-tar-gz.json`) + } + private async setChannel() { const channelPath = path.join(this.options.config.dataDir, 'channel') fs.writeFile(channelPath, this.channel, 'utf8') diff --git a/test/update.test.ts b/test/update.test.ts index 94080fce..3f53c2b4 100644 --- a/test/update.test.ts +++ b/test/update.test.ts @@ -37,6 +37,7 @@ function initUpdateCli(options: Partial): UpdateCli { fromLocal: options.fromLocal || false, autoUpdate: options.autoUpdate || false, config: options.config!, + version: undefined, exit: undefined, getPinToVersion: async () => '2.0.0', }) From e44820c1b5df9dbae8974be3eb8a02c504a5d28f Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Tue, 1 Feb 2022 11:12:46 -0700 Subject: [PATCH 02/17] feat: add --interactive --- package.json | 10 +- src/commands/update.ts | 54 +++++++-- src/tar.ts | 2 +- src/update.ts | 206 ++++++++++++++------------------ src/util.ts | 4 +- test/update.test.ts | 58 +++++++-- yarn.lock | 265 +++++++++++++++++++---------------------- 7 files changed, 310 insertions(+), 289 deletions(-) diff --git a/package.json b/package.json index 5a13f3c2..0a7158af 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,12 @@ "bugs": "https://github.com/oclif/plugin-update/issues", "dependencies": { "@oclif/color": "^1.0.0", - "@oclif/core": "^1.2.0", - "@types/semver": "^7.3.4", - "cross-spawn": "^7.0.3", + "@oclif/core": "^1.3.0", "debug": "^4.3.1", "filesize": "^6.1.0", "fs-extra": "^9.0.1", "http-call": "^5.3.0", + "inquirer": "^8.2.0", "lodash": "^4.17.21", "log-chopper": "^1.0.2", "semver": "^7.3.5", @@ -21,13 +20,14 @@ "@oclif/plugin-help": "^5.1.9", "@oclif/test": "^2.0.2", "@types/chai": "^4.2.15", - "@types/cross-spawn": "^6.0.2", "@types/execa": "^0.9.0", "@types/fs-extra": "^8.0.1", "@types/glob": "^7.1.3", + "@types/inquirer": "^8.2.0", "@types/lodash": "^4.14.168", "@types/mocha": "^9", "@types/node": "^14.14.31", + "@types/semver": "^7.3.4", "@types/supports-color": "^7.2.0", "@types/write-json-file": "^3.2.1", "chai": "^4.3.4", @@ -37,7 +37,7 @@ "globby": "^11.0.2", "mocha": "^9", "nock": "^13.2.1", - "oclif": "^2.3.0", + "oclif": "^2.4.2", "qqjs": "^0.3.11", "sinon": "^12.0.1", "ts-node": "^9.1.1", diff --git a/src/commands/update.ts b/src/commands/update.ts index eda699dc..33f9af29 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -1,11 +1,8 @@ -import {Command, Flags, Config, CliUx} from '@oclif/core' -import * as path from 'path' +import {Command, Flags, Config} from '@oclif/core' +import {prompt} from 'inquirer' +import {sort} from 'semver' import UpdateCli from '../update' -async function getPinToVersion(): Promise { - return CliUx.ux.prompt('Enter a version to update to') -} - export default class UpdateCommand extends Command { static description = 'update the <%= config.bin %> CLI' @@ -13,15 +10,20 @@ export default class UpdateCommand extends Command { static flags = { autoupdate: Flags.boolean({hidden: true}), - 'from-local': Flags.boolean({description: 'Interactively choose an already installed version.'}), + 'from-local': Flags.boolean({ + description: 'Interactively choose an already installed version. This is ignored if a channel is provided.', + exclusive: ['version', 'interactive'], + }), version: Flags.string({ description: 'Install a specific version.', - exclusive: ['from-local'], + exclusive: ['from-local', 'interactive'], + }), + interactive: Flags.boolean({ + description: 'Interactively select version to install. This is ignored if a channel is provided.', + exclusive: ['from-local', 'version'], }), } - private readonly clientRoot = this.config.scopedEnvVar('OCLIF_CLIENT_HOME') || path.join(this.config.dataDir, 'client') - async run(): Promise { const {args, flags} = await this.parse(UpdateCommand) @@ -29,15 +31,43 @@ export default class UpdateCommand extends Command { this.error('You cannot specifiy both a version and a channel.') } + let version = flags.version + if (flags['from-local']) { + version = await this.promptForLocalVersion() + } else if (flags.interactive) { + version = await this.promptForRemoteVersion() + } + const updateCli = new UpdateCli({ channel: args.channel, autoUpdate: flags.autoupdate, fromLocal: flags['from-local'], - version: flags.version, + version, config: this.config as Config, exit: this.exit, - getPinToVersion: getPinToVersion, }) return updateCli.runUpdate() } + + private async promptForRemoteVersion(): Promise { + const choices = sort(Object.keys(await UpdateCli.fetchVersionIndex(this.config))).reverse() + const {version} = await prompt<{version: string}>({ + name: 'version', + message: 'Select a version to update to', + type: 'list', + choices, + }) + return version + } + + private async promptForLocalVersion(): Promise { + const choices = sort(UpdateCli.findLocalVersions(this.config)).reverse() + const {version} = await prompt<{version: string}>({ + name: 'version', + message: 'Select a version to update to', + type: 'list', + choices, + }) + return version + } } diff --git a/src/tar.ts b/src/tar.ts index 5af5b696..8a6f55c2 100644 --- a/src/tar.ts +++ b/src/tar.ts @@ -18,7 +18,7 @@ const ignore = (_: any, header: any) => { } } -export async function extract(stream: NodeJS.ReadableStream, basename: string, output: string, sha?: string) { +export async function extract(stream: NodeJS.ReadableStream, basename: string, output: string, sha?: string): Promise { const getTmp = () => `${output}.partial.${Math.random().toString().split('.')[1].slice(0, 5)}` let tmp = getTmp() if (fs.pathExistsSync(tmp)) tmp = getTmp() diff --git a/src/update.ts b/src/update.ts index e80119e7..8ba4c5a2 100644 --- a/src/update.ts +++ b/src/update.ts @@ -1,9 +1,8 @@ /* eslint-disable unicorn/prefer-module */ import color from '@oclif/color' -import {Config, CliUx} from '@oclif/core' -import {IManifest} from 'oclif' +import {Config, CliUx, Interfaces} from '@oclif/core' -import * as spawn from 'cross-spawn' +// import * as spawn from 'cross-spawn' import * as fs from 'fs-extra' import HTTP from 'http-call' import * as _ from 'lodash' @@ -18,12 +17,17 @@ export interface UpdateCliOptions { fromLocal: boolean; version: string | undefined; config: Config; - exit: any; - getPinToVersion: () => Promise; + exit: (code?: number | undefined) => void; } export type VersionIndex = Record +function composeS3SubDir(config: Config): string { + let s3SubDir = (config.pjson.oclif.update.s3 as any).folder || '' + if (s3SubDir !== '' && s3SubDir.slice(-1) !== '/') s3SubDir = `${s3SubDir}/` + return s3SubDir +} + export default class UpdateCli { private channel!: string @@ -35,9 +39,58 @@ export default class UpdateCli { private readonly clientBin: string + public static findLocalVersions(config: Config): string[] { + const clientRoot = UpdateCli.getClientRoot(config) + const versions = fs.readdirSync(clientRoot).filter(dirOrFile => dirOrFile !== 'bin' && dirOrFile !== 'current') + if (versions.length === 0) throw new Error('No locally installed versions found.') + return versions + } + + public static async fetchVersionIndex(config: Config): Promise { + const http: typeof HTTP = require('http-call').HTTP + + CliUx.ux.action.status = 'fetching version index' + const newIndexUrl = config.s3Url( + UpdateCli.s3VersionIndexKey(config), + ) + + const {body} = await http.get(newIndexUrl) + if (typeof body === 'string') { + return JSON.parse(body) + } + + return body + } + + private static s3ChannelManifestKey(config: Config, channel: string): string { + const {bin, platform, arch} = config + const s3SubDir = composeS3SubDir(config) + return path.join(s3SubDir, 'channels', channel, `${bin}-${platform}-${arch}-buildmanifest`) + } + + private static s3VersionManifestKey(config: Config, version: string, hash: string): string { + const {bin, platform, arch} = config + const s3SubDir = composeS3SubDir(config) + return path.join(s3SubDir, 'versions', version, hash, `${bin}-v${version}-${hash}-${platform}-${arch}-buildmanifest`) + } + + private static s3VersionIndexKey(config: Config): string { + const {bin, platform, arch} = config + const s3SubDir = composeS3SubDir(config) + return path.join(s3SubDir, 'versions', `${bin}-${platform}-${arch}-tar-gz.json`) + } + + private static getClientRoot(config: Config): string { + return config.scopedEnvVar('OCLIF_CLIENT_HOME') || path.join(config.dataDir, 'client') + } + + private static getClientBin(config: Config): string { + return path.join(UpdateCli.getClientRoot(config), 'bin', config.windows ? `${config.bin}.cmd` : config.bin) + } + constructor(private options: UpdateCliOptions) { - this.clientRoot = this.options.config.scopedEnvVar('OCLIF_CLIENT_HOME') || path.join(this.options.config.dataDir, 'client') - this.clientBin = path.join(this.clientRoot, 'bin', this.options.config.windows ? `${this.options.config.bin}.cmd` : this.options.config.bin) + this.clientRoot = UpdateCli.getClientRoot(options.config) + this.clientBin = UpdateCli.getClientBin(options.config) } async runUpdate(): Promise { @@ -47,32 +100,22 @@ export default class UpdateCli { if (this.options.fromLocal) { await this.ensureClientDir() - CliUx.ux.debug(`Looking for locally installed versions at ${this.clientRoot}`) - - // Do not show known non-local version folder names, bin and current. - const versions = fs.readdirSync(this.clientRoot).filter(dirOrFile => dirOrFile !== 'bin' && dirOrFile !== 'current') - if (versions.length === 0) throw new Error('No locally installed versions found.') - - CliUx.ux.log(`Found versions: \n${versions.map(version => ` ${version}`).join('\n')}\n`) - - const pinToVersion = await this.options.getPinToVersion() - if (!versions.includes(pinToVersion)) throw new Error(`Version ${pinToVersion} not found in the locally installed versions.`) - - if (!await fs.pathExists(path.join(this.clientRoot, pinToVersion))) { - throw new Error(`Version ${pinToVersion} is not already installed at ${this.clientRoot}.`) + const version = this.options.version! + if (!await fs.pathExists(path.join(this.clientRoot, version))) { + throw new Error(`Version ${version} is not already installed at ${this.clientRoot}.`) } CliUx.ux.action.start(`${this.options.config.name}: Updating CLI`) - CliUx.ux.debug(`switching to existing version ${pinToVersion}`) - this.updateToExistingVersion(pinToVersion) + CliUx.ux.debug(`switching to existing version ${version}`) + this.updateToExistingVersion(version) CliUx.ux.log() 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}.`) } else if (this.options.version) { CliUx.ux.action.start(`${this.options.config.name}: Updating CLI`) - await this.options.config.runHook('preupdate', {channel: this.channel}) + await this.options.config.runHook('preupdate', {channel: this.channel, version: this.options.version}) - const index = await this.fetchVersionIndex() + const index = await UpdateCli.fetchVersionIndex(this.options.config) const url = index[this.options.version] if (!url) { throw new Error(`${this.options.version} not found in index:\n${Object.keys(index).join(', ')}`) @@ -87,58 +130,49 @@ export default class UpdateCli { CliUx.ux.debug('tidy') await this.tidy() - await this.options.config.runHook('update', {channel: this.channel}) + await this.options.config.runHook('update', {channel: this.channel, version: this.updatedVersion}) + + CliUx.ux.log() + 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 ${this.channel}.`) } else { CliUx.ux.action.start(`${this.options.config.name}: Updating CLI`) - await this.options.config.runHook('preupdate', {channel: this.channel}) const manifest = await this.fetchChannelManifest() this.currentVersion = await this.determineCurrentVersion() this.updatedVersion = manifest.sha ? `${manifest.version}-${manifest.sha}` : manifest.version + await this.options.config.runHook('preupdate', {channel: this.channel, version: this.updatedVersion}) const reason = await this.skipUpdate() if (reason) CliUx.ux.action.stop(reason || 'done') else await this.update(manifest) CliUx.ux.debug('tidy') await this.tidy() - await this.options.config.runHook('update', {channel: this.channel}) + await this.options.config.runHook('update', {channel: this.channel, version: this.updatedVersion}) } CliUx.ux.debug('done') CliUx.ux.action.stop() } - private async fetchChannelManifest(): Promise { - const s3Key = this.s3ChannelManifestKey( - this.options.config.bin, - this.options.config.platform, - this.options.config.arch, - (this.options.config.pjson.oclif.update.s3 as any).folder, - ) + private async fetchChannelManifest(): Promise { + const s3Key = UpdateCli.s3ChannelManifestKey(this.options.config, this.channel) return this.fetchManifest(s3Key) } - private async fetchVersionManifest(version: string, url: string): Promise { + private async fetchVersionManifest(version: string, url: string): Promise { const parts = url.split('/') const hashIndex = parts.indexOf(version) + 1 const hash = parts[hashIndex] - const s3Key = this.s3VersionManifestKey( - this.options.config.bin, - version, - hash, - this.options.config.platform, - this.options.config.arch, - (this.options.config.pjson.oclif.update.s3 as any).folder, - ) + const s3Key = UpdateCli.s3VersionManifestKey(this.options.config, version, hash) return this.fetchManifest(s3Key) } - private async fetchManifest(s3Key: string): Promise { + private async fetchManifest(s3Key: string): Promise { const http: typeof HTTP = require('http-call').HTTP CliUx.ux.action.status = 'fetching manifest' try { const url = this.options.config.s3Url(s3Key) - const {body} = await http.get(url) + const {body} = await http.get(url) if (typeof body === 'string') { return JSON.parse(body) } @@ -150,29 +184,7 @@ export default class UpdateCli { } } - private async fetchVersionIndex(): Promise { - const http: typeof HTTP = require('http-call').HTTP - - CliUx.ux.action.status = 'fetching version index' - - const newIndexUrl = this.options.config.s3Url( - this.s3VersionIndexKey( - this.options.config.bin, - this.options.config.platform, - this.options.config.arch, - (this.options.config.pjson.oclif.update.s3 as any).folder, - ), - ) - - const {body} = await http.get(newIndexUrl) - if (typeof body === 'string') { - return JSON.parse(body) - } - - return body - } - - private async downloadAndExtract(output: string, manifest: IManifest, channel: string) { + private async downloadAndExtract(output: string, manifest: Interfaces.S3Manifest, channel: string) { const {version, gz, sha256gz} = manifest const filesize = (n: number): string => { @@ -223,7 +235,7 @@ export default class UpdateCli { await extraction } - private async update(manifest: IManifest, channel = 'stable') { + private async update(manifest: Interfaces.S3Manifest, channel = 'stable') { CliUx.ux.action.start(`${this.options.config.name}: Updating CLI from ${color.green(this.currentVersion)} to ${color.green(this.updatedVersion)}${channel === 'stable' ? '' : ' (' + color.yellow(channel) + ')'}`) await this.ensureClientDir() @@ -236,10 +248,10 @@ export default class UpdateCli { await this.setChannel() await this.createBin(this.updatedVersion) await this.touch() - await this.reexec() + CliUx.ux.action.stop() } - private async updateToExistingVersion(version: string) { + private async updateToExistingVersion(version: string): Promise { await this.createBin(version) await this.touch() } @@ -281,31 +293,12 @@ export default class UpdateCli { return this.options.config.version } - private s3ChannelManifestKey(bin: string, platform: string, arch: string, folder = ''): string { - let s3SubDir = folder || '' - if (s3SubDir !== '' && s3SubDir.slice(-1) !== '/') s3SubDir = `${s3SubDir}/` - return path.join(s3SubDir, 'channels', this.channel, `${bin}-${platform}-${arch}-buildmanifest`) - } - - // eslint-disable-next-line max-params - private s3VersionManifestKey(bin: string, version: string, hash: string, platform: string, arch: string, folder = ''): string { - let s3SubDir = folder || '' - if (s3SubDir !== '' && s3SubDir.slice(-1) !== '/') s3SubDir = `${s3SubDir}/` - return path.join(s3SubDir, 'versions', version, hash, `${bin}-v${version}-${hash}-${platform}-${arch}-buildmanifest`) - } - - private s3VersionIndexKey(bin: string, platform: string, arch: string, folder = ''): string { - let s3SubDir = folder || '' - if (s3SubDir !== '' && s3SubDir.slice(-1) !== '/') s3SubDir = `${s3SubDir}/` - return path.join(s3SubDir, 'versions', `${bin}-${platform}-${arch}-tar-gz.json`) - } - - private async setChannel() { + private async setChannel(): Promise { const channelPath = path.join(this.options.config.dataDir, 'channel') fs.writeFile(channelPath, this.channel, 'utf8') } - private async logChop() { + private async logChop(): Promise { try { CliUx.ux.debug('log chop') const logChopper = require('log-chopper').default @@ -315,7 +308,7 @@ export default class UpdateCli { } } - private async mtime(f: string) { + private async mtime(f: string): Promise { const {mtime} = await fs.stat(f) return mtime } @@ -343,7 +336,7 @@ export default class UpdateCli { } // removes any unused CLIs - private async tidy() { + private async tidy(): Promise { try { const root = this.clientRoot if (!await fs.pathExists(root)) return @@ -363,7 +356,7 @@ export default class UpdateCli { } } - private async touch() { + private async touch(): Promise { // touch the client so it won't be tidied up right away try { const p = path.join(this.clientRoot, this.options.config.version) @@ -375,26 +368,7 @@ export default class UpdateCli { } } - private async reexec() { - CliUx.ux.action.stop() - return new Promise((_, reject) => { - CliUx.ux.debug('restarting CLI after update', this.clientBin) - spawn(this.clientBin, ['update'], { - stdio: 'inherit', - env: {...process.env, [this.options.config.scopedEnvVarKey('HIDE_UPDATED_MESSAGE')]: '1'}, - }) - .on('error', reject) - .on('close', (status: number) => { - try { - if (status > 0) this.options.exit(status) - } catch (error: any) { - reject(error) - } - }) - }) - } - - private async createBin(version: string) { + private async createBin(version: string): Promise { const dst = this.clientBin const {bin, windows} = this.options.config const binPathEnvVar = this.options.config.scopedEnvVarKey('BINPATH') @@ -436,7 +410,7 @@ ${binPathEnvVar}="\$DIR/${bin}" ${redirectedEnvVar}=1 "$DIR/../${version}/bin/${ } } - private async ensureClientDir() { + private async ensureClientDir(): Promise { try { await fs.mkdirp(this.clientRoot) } catch (error: any) { diff --git a/src/util.ts b/src/util.ts index 070617cd..c135c6a3 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,7 +1,7 @@ import * as fs from 'fs-extra' import * as path from 'path' -export async function touch(p: string) { +export async function touch(p: string): Promise { try { await fs.utimes(p, new Date(), new Date()) } catch { @@ -9,7 +9,7 @@ export async function touch(p: string) { } } -export async function ls(dir: string) { +export async function ls(dir: string): Promise> { const files = await fs.readdir(dir) const paths = files.map(f => path.join(dir, f)) return Promise.all(paths.map(path => fs.stat(path).then(stat => ({path, stat})))) diff --git a/test/update.test.ts b/test/update.test.ts index 3f53c2b4..855c28c8 100644 --- a/test/update.test.ts +++ b/test/update.test.ts @@ -37,9 +37,10 @@ function initUpdateCli(options: Partial): UpdateCli { fromLocal: options.fromLocal || false, autoUpdate: options.autoUpdate || false, config: options.config!, - version: undefined, - exit: undefined, - getPinToVersion: async () => '2.0.0', + version: options.version, + exit: () => { + // do nothing + }, }) expect(updateCli).to.be.ok return updateCli @@ -80,14 +81,12 @@ describe('update plugin', () => { .get(manifestRegex) .reply(200, {version: '2.0.0'}) - sandbox.stub(UpdateCli.prototype, 'reexec' as any).resolves() - updateCli = initUpdateCli({config: config! as Config}) await updateCli.runUpdate() const stdout = collector.stdout.join(' ') expect(stdout).to.include('already on latest version') }) - it('should update', async () => { + it('should update to channel', async () => { clientRoot = setupClientRoot({config}) const platformRegex = new RegExp(`tarballs\\/example-cli\\/${config.platform}-${config.arch}`) const manifestRegex = new RegExp(`channels\\/stable\\/example-cli-${config.platform}-${config.arch}-buildmanifest`) @@ -97,7 +96,6 @@ describe('update plugin', () => { fs.mkdirpSync(path.join(`${newVersionPath}.partial.11111`, 'bin')) fs.writeFileSync(path.join(`${newVersionPath}.partial.11111`, 'bin', 'example-cli'), '../2.0.1/bin', 'utf8') // fs.writeFileSync(path.join(newVersionPath, 'bin', 'example-cli'), '../2.0.1/bin', 'utf8') - sandbox.stub(UpdateCli.prototype, 'reexec' as any).resolves() sandbox.stub(extract, 'extract').resolves() sandbox.stub(zlib, 'gzipSync').returns(Buffer.alloc(1, ' ')) @@ -120,6 +118,47 @@ describe('update plugin', () => { const stdout = stripAnsi(collector.stdout.join(' ')) expect(stdout).to.matches(/Updating CLI from 2.0.0 to 2.0.1/) }) + it('should update to version', async () => { + const hash = 'f289627' + clientRoot = setupClientRoot({config}) + const platformRegex = new RegExp(`tarballs\\/example-cli\\/${config.platform}-${config.arch}`) + const manifestRegex = new RegExp(`channels\\/stable\\/example-cli-${config.platform}-${config.arch}-buildmanifest`) + const versionManifestRegex = new RegExp(`example-cli-v2.0.1-${hash}-${config.platform}-${config.arch}-buildmanifest`) + const tarballRegex = new RegExp(`tarballs\\/example-cli\\/example-cli-v2.0.1\\/example-cli-v2.0.1-${config.platform}-${config.arch}gz`) + const indexRegex = new RegExp(`example-cli-${config.platform}-${config.arch}-tar-gz.json`) + const newVersionPath = path.join(clientRoot, '2.0.1') + // fs.mkdirpSync(path.join(newVersionPath, 'bin')) + fs.mkdirpSync(path.join(`${newVersionPath}.partial.11111`, 'bin')) + fs.writeFileSync(path.join(`${newVersionPath}.partial.11111`, 'bin', 'example-cli'), '../2.0.1/bin', 'utf8') + // fs.writeFileSync(path.join(newVersionPath, 'bin', 'example-cli'), '../2.0.1/bin', 'utf8') + sandbox.stub(extract, 'extract').resolves() + sandbox.stub(zlib, 'gzipSync').returns(Buffer.alloc(1, ' ')) + + const gzContents = zlib.gzipSync(' ') + + nock(/oclif-staging.s3.amazonaws.com/) + .get(platformRegex) + .reply(200, {version: '2.0.1'}) + .get(manifestRegex) + .reply(200, {version: '2.0.1'}) + .get(versionManifestRegex) + .reply(200, {version: '2.0.1'}) + .get(tarballRegex) + .reply(200, gzContents, { + 'X-Transfer-Length': String(gzContents.length), + 'content-length': String(gzContents.length), + 'Content-Encoding': 'gzip', + }) + .get(indexRegex) + .reply(200, { + '2.0.1': `versions/example-cli/2.0.1/${hash}/example-cli-v2.0.1-${config.platform}-${config.arch}.gz`, + }) + + updateCli = initUpdateCli({config: config as Config, version: '2.0.1'}) + await updateCli.runUpdate() + const stdout = stripAnsi(collector.stdout.join(' ')) + expect(stdout).to.matches(/Updating CLI from 2.0.0 to 2.0.1/) + }) it('should not update - not updatable', async () => { clientRoot = setupClientRoot({config}) // unset binPath @@ -130,8 +169,6 @@ describe('update plugin', () => { .get(/channels\/stable\/example-cli-.+?-buildmanifest/) .reply(200, {version: '2.0.0'}) - sandbox.stub(UpdateCli.prototype, 'reexec' as any).resolves() - updateCli = initUpdateCli({config: config as Config}) await updateCli.runUpdate() const stdout = collector.stdout.join(' ') @@ -147,7 +184,6 @@ describe('update plugin', () => { fs.mkdirpSync(path.join(`${newVersionPath}.partial.11111`, 'bin')) fs.writeFileSync(path.join(`${newVersionPath}.partial.11111`, 'bin', 'example-cli'), '../2.0.0/bin', 'utf8') fs.writeFileSync(path.join(newVersionPath, 'bin', 'example-cli'), '../2.0.0/bin', 'utf8') - sandbox.stub(UpdateCli.prototype, 'reexec' as any).resolves() sandbox.stub(extract, 'extract').resolves() sandbox.stub(zlib, 'gzipSync').returns(Buffer.alloc(1, ' ')) @@ -165,7 +201,7 @@ describe('update plugin', () => { 'Content-Encoding': 'gzip', }) - updateCli = initUpdateCli({fromLocal: true, config: config as Config, getPinToVersion: async () => '2.0.0'}) + updateCli = initUpdateCli({fromLocal: true, config: config as Config, version: '2.0.0'}) await updateCli.runUpdate() const stdout = stripAnsi(collector.stdout.join(' ')) expect(stdout).to.matches(/Updating to an already installed version will not update the channel/) diff --git a/yarn.lock b/yarn.lock index a00c9ec4..455e4cdb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -432,7 +432,7 @@ supports-color "^8.1.1" tslib "^2" -"@oclif/core@1.0.10", "@oclif/core@^1.0.10", "@oclif/core@^1.0.8": +"@oclif/core@^1.0.10", "@oclif/core@^1.0.8": version "1.0.10" resolved "https://registry.yarnpkg.com/@oclif/core/-/core-1.0.10.tgz#5fd01d572e44d372b7279ee0f49b4860e14b6e4e" integrity sha512-L+IcNU3NoYxwz5hmHfcUlOJ3dpgHRsIj1kAmI9CKEJHq5gBVKlP44Ot179Jke1jKRKX2g9N42izbmlh0SNpkkw== @@ -455,10 +455,10 @@ widest-line "^3.1.0" wrap-ansi "^7.0.0" -"@oclif/core@^1.0.11", "@oclif/core@^1.2.0": - version "1.2.0" - resolved "https://registry.npmjs.org/@oclif/core/-/core-1.2.0.tgz#f1110b1fe868e439f94f8b4ffad5dd8acf862294" - integrity sha512-h1n8NEAUzaL3+wky7W1FMeySmJWQpYX1LhWMltFY/ScvmapZzee7D9kzy/XI/ZIWWfz2ZYCTMD1wOKXO6ueynw== +"@oclif/core@^1.2.1", "@oclif/core@^1.3.0": + version "1.3.0" + resolved "https://registry.npmjs.org/@oclif/core/-/core-1.3.0.tgz#f0547d2ca9b13c2a54f1c1d88e03a8c8ca7799bb" + integrity sha512-YSy1N3SpOn/8vmY8lllmTzQ4+KGjTlyFoNr/PxvebuYxo0iO0uQSpXIr8qDoNGQUTy+3Z5feUxoV04uUgAlI6Q== dependencies: "@oclif/linewrap" "^1.0.0" "@oclif/screen" "^3.0.2" @@ -490,29 +490,6 @@ widest-line "^3.1.0" wrap-ansi "^7.0.0" -"@oclif/core@^1.1.1": - version "1.1.1" - resolved "https://registry.npmjs.org/@oclif/core/-/core-1.1.1.tgz#71a91be5af645a7088d4f716a9c83b0adbd1d4a3" - integrity sha512-lCn4CT39gMV9oo/P1u99kmBy61RU5lsq0ENocnnvoFtGHrsEZQgOztxR6mTBKMl5QCBK1c6cEy47E8owUQgEGw== - dependencies: - "@oclif/linewrap" "^1.0.0" - chalk "^4.1.2" - clean-stack "^3.0.1" - cli-ux "^6.0.6" - debug "^4.3.3" - fs-extra "^9.1.0" - get-package-type "^0.1.0" - globby "^11.0.4" - indent-string "^4.0.0" - is-wsl "^2.2.0" - lodash "^4.17.21" - semver "^7.3.5" - string-width "^4.2.3" - strip-ansi "^6.0.1" - tslib "^2.3.1" - widest-line "^3.1.0" - wrap-ansi "^7.0.0" - "@oclif/linewrap@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@oclif/linewrap/-/linewrap-1.0.0.tgz#aedcb64b479d4db7be24196384897b5000901d91" @@ -525,18 +502,17 @@ dependencies: "@oclif/core" "^1.0.10" -"@oclif/plugin-not-found@^2.2.3": - version "2.2.4" - resolved "https://registry.npmjs.org/@oclif/plugin-not-found/-/plugin-not-found-2.2.4.tgz#fdea729face92228c19fc2bf04759909e9a986b1" - integrity sha512-ESq4nqgtWXpF2zCjhYC+dOTcStHu9mHttmFjElSvRUPOLqL38TGwqcX07ATzw/OQAsonDVwIaH/neSfq+MmWgA== +"@oclif/plugin-not-found@^2.2.4": + version "2.3.1" + resolved "https://registry.npmjs.org/@oclif/plugin-not-found/-/plugin-not-found-2.3.1.tgz#8fe1019fdeeb77be055314662bb9180808222e80" + integrity sha512-AeNBw+zSkRpePmpXO8xlL072VF2/R2yK3qsVs/JF26Yw1w77TWuRTdFR+hFotJtFCJ4QYqhNtKSjdryCO9AXsA== dependencies: "@oclif/color" "^1.0.0" - "@oclif/core" "^1.1.1" - cli-ux "^6.0.6" + "@oclif/core" "^1.2.1" fast-levenshtein "^3.0.0" lodash "^4.17.21" -"@oclif/plugin-warn-if-update-available@^2.0.3": +"@oclif/plugin-warn-if-update-available@^2.0.4": version "2.0.4" resolved "https://registry.npmjs.org/@oclif/plugin-warn-if-update-available/-/plugin-warn-if-update-available-2.0.4.tgz#3d509ca2394cccf65e6622be812d7be4065a60aa" integrity sha512-9dprC1CWPjesg0Vf/rDSQH2tzJXhP1ow84cb2My1kj6e6ESulPKpctiCFSZ1WaCQFfq+crKhzlNoP/vRaXNUAg== @@ -722,13 +698,6 @@ resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== -"@types/cross-spawn@^6.0.2": - version "6.0.2" - resolved "https://registry.yarnpkg.com/@types/cross-spawn/-/cross-spawn-6.0.2.tgz#168309de311cd30a2b8ae720de6475c2fbf33ac7" - integrity sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw== - dependencies: - "@types/node" "*" - "@types/execa@^0.9.0": version "0.9.0" resolved "https://registry.yarnpkg.com/@types/execa/-/execa-0.9.0.tgz#9b025d2755f17e80beaf9368c3f4f319d8b0fb93" @@ -756,6 +725,14 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/inquirer@^8.2.0": + version "8.2.0" + resolved "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.0.tgz#b9566d048f5ff65159f2ed97aff45fe0f00b35ec" + integrity sha512-BNoMetRf3gmkpAlV5we+kxyZTle7YibdOntIZbU5pyIfMdcwy784KfeZDAcuyMznkh5OLa17RVXZOGA5LTlkgQ== + dependencies: + "@types/through" "*" + rxjs "^7.2.0" + "@types/json-schema@^7.0.7": version "7.0.9" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" @@ -818,6 +795,13 @@ resolved "https://registry.yarnpkg.com/@types/supports-color/-/supports-color-7.2.0.tgz#edd98ae52ee786b733a5dea0a23da4eb18ef7310" integrity sha512-gtUcOP6qIpjbSDdWjMBRNSks42ccx1709mwKTgelW63BESIADw8Ju7klpydDDb9Kr0iRXfpwrXH8+zoU8TCqiA== +"@types/through@*": + version "0.0.30" + resolved "https://registry.npmjs.org/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895" + integrity sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg== + dependencies: + "@types/node" "*" + "@types/vinyl@^2.0.4": version "2.0.6" resolved "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.6.tgz#b2d134603557a7c3d2b5d3dc23863ea2b5eb29b0" @@ -1148,12 +1132,27 @@ at-least-node@^1.0.0: resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== +aws-sdk@^2.1064.0: + version "2.1066.0" + resolved "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1066.0.tgz#2a9b00d983f3c740a7adda18d4e9a5c34d4d3887" + integrity sha512-9BZPdJgIvau8Jf2l3PxInNqQd733uKLqGGDywMV71duxNTLgdBZe2zvCkbgl22+ldC8R2LVMdS64DzchfQIxHg== + dependencies: + buffer "4.9.2" + events "1.1.1" + ieee754 "1.1.13" + jmespath "0.16.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.3.2" + xml2js "0.4.19" + balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= -base64-js@^1.3.1: +base64-js@^1.0.2, base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -1239,6 +1238,15 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +buffer@4.9.2: + version "4.9.2" + resolved "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" + integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + buffer@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" @@ -1506,68 +1514,6 @@ cli-ux@6.0.5: supports-hyperlinks "^2.1.0" tslib "^2.0.0" -cli-ux@^6.0.6: - version "6.0.6" - resolved "https://registry.npmjs.org/cli-ux/-/cli-ux-6.0.6.tgz#00536bf6038f195b0a1a2589f61ce5e625e75870" - integrity sha512-CvL4qmV78VhnbyHTswGjpDSQtU+oj3hT9DP9L6yMOwiTiNv0nMjMEV/8zou4CSqO6PtZ2A8qnlZDgAc07Js+aw== - dependencies: - "@oclif/core" "1.0.10" - "@oclif/linewrap" "^1.0.0" - "@oclif/screen" "^1.0.4 " - ansi-escapes "^4.3.0" - ansi-styles "^4.2.0" - cardinal "^2.1.1" - chalk "^4.1.0" - clean-stack "^3.0.0" - cli-progress "^3.9.1" - extract-stack "^2.0.0" - fs-extra "^8.1" - hyperlinker "^1.0.0" - indent-string "^4.0.0" - is-wsl "^2.2.0" - js-yaml "^3.13.1" - lodash "^4.17.21" - natural-orderby "^2.0.1" - object-treeify "^1.1.4" - password-prompt "^1.1.2" - semver "^7.3.2" - string-width "^4.2.0" - strip-ansi "^6.0.0" - supports-color "^8.1.0" - supports-hyperlinks "^2.1.0" - tslib "^2.0.0" - -cli-ux@^6.0.8: - version "6.0.8" - resolved "https://registry.npmjs.org/cli-ux/-/cli-ux-6.0.8.tgz#a3943e889df827ceab2ffbe3e46c1fff194099e9" - integrity sha512-ERJ61QDVS1fqnWhzp3cPFHbfucmkzWh/K6SlMlf5GweIb0wB4G/wtZiAeWK6TOTSFXGQdGczVHzWrG1BMqTmSw== - dependencies: - "@oclif/core" "^1.1.1" - "@oclif/linewrap" "^1.0.0" - "@oclif/screen" "^1.0.4 " - ansi-escapes "^4.3.0" - ansi-styles "^4.2.0" - cardinal "^2.1.1" - chalk "^4.1.0" - clean-stack "^3.0.0" - cli-progress "^3.10.0" - extract-stack "^2.0.0" - fs-extra "^8.1" - hyperlinker "^1.0.0" - indent-string "^4.0.0" - is-wsl "^2.2.0" - js-yaml "^3.13.1" - lodash "^4.17.21" - natural-orderby "^2.0.1" - object-treeify "^1.1.4" - password-prompt "^1.1.2" - semver "^7.3.2" - string-width "^4.2.0" - strip-ansi "^6.0.0" - supports-color "^8.1.0" - supports-hyperlinks "^2.1.0" - tslib "^2.0.0" - cli-width@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" @@ -1950,11 +1896,6 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-walk@^0.1.0: - version "0.1.2" - resolved "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84" - integrity sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w== - ejs@^3.1.6: version "3.1.6" resolved "https://registry.npmjs.org/ejs/-/ejs-3.1.6.tgz#5bfd0a0689743bb5268b3550cceeebbc1702822a" @@ -2284,6 +2225,11 @@ eventemitter3@^4.0.4: resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== +events@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= + execa@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50" @@ -2743,14 +2689,6 @@ glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" -global@^4.4.0: - version "4.4.0" - resolved "https://registry.npmjs.org/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406" - integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w== - dependencies: - min-document "^2.19.0" - process "^0.11.10" - globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -2975,7 +2913,12 @@ iconv-lite@^0.6.2: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" -ieee754@^1.1.13: +ieee754@1.1.13: + version "1.1.13" + resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + +ieee754@^1.1.13, ieee754@^1.1.4: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -3050,7 +2993,7 @@ inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -inquirer@^8.0.0: +inquirer@^8.0.0, inquirer@^8.2.0: version "8.2.0" resolved "https://registry.npmjs.org/inquirer/-/inquirer-8.2.0.tgz#f44f008dd344bbfc4b30031f45d984e034a3ac3a" integrity sha512-0crLweprevJ02tTuA6ThpoAERAGyVILC4sS74uib58Xf/zSr1/ZWtmm7D5CI+bSQEaA04f0K7idaHpQbSWgiVQ== @@ -3231,7 +3174,7 @@ isarray@0.0.1: resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= -isarray@~1.0.0: +isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= @@ -3256,6 +3199,11 @@ jake@^10.6.1: filelist "^1.0.1" minimatch "^3.0.4" +jmespath@0.16.0: + version "0.16.0" + resolved "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz#b15b0a85dfd4d930d43e69ed605943c802785076" + integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -3632,13 +3580,6 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -min-document@^2.19.0: - version "2.19.0" - resolved "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" - integrity sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU= - dependencies: - dom-walk "^0.1.0" - "minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -4060,21 +4001,20 @@ object-treeify@^1.1.4: resolved "https://registry.yarnpkg.com/object-treeify/-/object-treeify-1.1.25.tgz#eb634c397bfc6512a28f569809079c93f41fe6d0" integrity sha512-6Abx0xlXDnYd50JkQefvoIly3jWOu8/PqH4lh8p2/aMFEx5TjsUGHt0H9NHfzt+pCwOhpPgNYofD8e2YywIXig== -oclif@^2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/oclif/-/oclif-2.3.0.tgz#ae344fad2666ba235642c72a87e67384b2574667" - integrity sha512-L2GRFlsWCl6XDhKqXCclTPdRovWRHQ5t0Vca9cIIc9WCtBe0hC8NQQ8gzwW/w9OH+LC7JaItR79DsCov9U1qPA== +oclif@^2.4.2: + version "2.4.2" + resolved "https://registry.npmjs.org/oclif/-/oclif-2.4.2.tgz#5eaf9bcff6470b39d1ea0bca5febe122d4627519" + integrity sha512-7TPlwXxjOqtCSfh3kpEz8V0Voza4eeucsoMB8ZWtFgOJ9AIpym+mwv7zOrmDXvS2/s/M3ht4a5lnRuTFSyKJMQ== dependencies: - "@oclif/core" "^1.0.11" + "@oclif/core" "^1.3.0" "@oclif/plugin-help" "^5.1.10" - "@oclif/plugin-not-found" "^2.2.3" - "@oclif/plugin-warn-if-update-available" "^2.0.3" - cli-ux "^6.0.8" + "@oclif/plugin-not-found" "^2.2.4" + "@oclif/plugin-warn-if-update-available" "^2.0.4" + aws-sdk "^2.1064.0" debug "^4.3.3" find-yarn-workspace-root "^2.0.0" fs-extra "^8.1" github-slugger "^1.2.1" - global "^4.4.0" lodash "^4.17.11" normalize-package-data "^3.0.3" nps-utils "^1.7.0" @@ -4446,11 +4386,6 @@ process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -process@^0.11.10: - version "0.11.10" - resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= - progress@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" @@ -4497,6 +4432,11 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= + punycode@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -4521,6 +4461,11 @@ qqjs@^0.3.10, qqjs@^0.3.11: tmp "^0.1.0" write-json-file "^4.1.1" +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= + ramda@^0.27.1: version "0.27.1" resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.1.tgz#66fc2df3ef873874ffc2da6aa8984658abacf5c9" @@ -4778,6 +4723,16 @@ safe-regex@^2.1.1: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sax@1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" + integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= + +sax@>=0.6.0: + version "1.2.4" + resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + scoped-regex@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/scoped-regex/-/scoped-regex-2.1.0.tgz#7b9be845d81fd9d21d1ec97c61a0b7cf86d2015f" @@ -5437,11 +5392,24 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +url@0.10.3: + version "0.10.3" + resolved "https://registry.npmjs.org/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" + integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +uuid@3.3.2: + version "3.3.2" + resolved "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + uuid@^2.0.1: version "2.0.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" @@ -5615,6 +5583,19 @@ write-json-file@*, write-json-file@^4.1.1: sort-keys "^4.0.0" write-file-atomic "^3.0.0" +xml2js@0.4.19: + version "0.4.19" + resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" + integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== + dependencies: + sax ">=0.6.0" + xmlbuilder "~9.0.1" + +xmlbuilder@~9.0.1: + version "9.0.7" + resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" + integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= + y18n@^5.0.5: version "5.0.5" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.5.tgz#8769ec08d03b1ea2df2500acef561743bbb9ab18" From 1f88195dae356e140444b0b0705bfdb95d2039b1 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Tue, 1 Feb 2022 11:15:25 -0700 Subject: [PATCH 03/17] chore: add back necessary deps --- package.json | 2 ++ src/update.ts | 1 - yarn.lock | 7 +++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 0a7158af..2381b877 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,8 @@ "dependencies": { "@oclif/color": "^1.0.0", "@oclif/core": "^1.3.0", + "@types/cross-spawn": "^6.0.2", + "cross-spawn": "^7.0.3", "debug": "^4.3.1", "filesize": "^6.1.0", "fs-extra": "^9.0.1", diff --git a/src/update.ts b/src/update.ts index 8ba4c5a2..7325a1a4 100644 --- a/src/update.ts +++ b/src/update.ts @@ -2,7 +2,6 @@ import color from '@oclif/color' import {Config, CliUx, Interfaces} from '@oclif/core' -// import * as spawn from 'cross-spawn' import * as fs from 'fs-extra' import HTTP from 'http-call' import * as _ from 'lodash' diff --git a/yarn.lock b/yarn.lock index 455e4cdb..4b529a0a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -698,6 +698,13 @@ resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== +"@types/cross-spawn@^6.0.2": + version "6.0.2" + resolved "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.2.tgz#168309de311cd30a2b8ae720de6475c2fbf33ac7" + integrity sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw== + dependencies: + "@types/node" "*" + "@types/execa@^0.9.0": version "0.9.0" resolved "https://registry.yarnpkg.com/@types/execa/-/execa-0.9.0.tgz#9b025d2755f17e80beaf9368c3f4f319d8b0fb93" From 68a6c0ed1fe471ae21d47a84e8b2a3ef68c2ed3b Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Tue, 1 Feb 2022 11:16:59 -0700 Subject: [PATCH 04/17] chore: move dep to devDependencies --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2381b877..8ee505ef 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,6 @@ "dependencies": { "@oclif/color": "^1.0.0", "@oclif/core": "^1.3.0", - "@types/cross-spawn": "^6.0.2", "cross-spawn": "^7.0.3", "debug": "^4.3.1", "filesize": "^6.1.0", @@ -22,6 +21,7 @@ "@oclif/plugin-help": "^5.1.9", "@oclif/test": "^2.0.2", "@types/chai": "^4.2.15", + "@types/cross-spawn": "^6.0.2", "@types/execa": "^0.9.0", "@types/fs-extra": "^8.0.1", "@types/glob": "^7.1.3", From 171ce46e4e3da970d0fd8cbcbef4cf68cc94a48b Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Tue, 1 Feb 2022 13:04:28 -0700 Subject: [PATCH 05/17] feat!: rename flags BREAKING CHANGE: rename --from-local to --local --- src/commands/update.ts | 49 ++++++++++++++++++++++++++++++++++++------ src/update.ts | 21 +++++++++++++----- src/util.ts | 7 ++++++ test/update.test.ts | 5 +++-- 4 files changed, 68 insertions(+), 14 deletions(-) diff --git a/src/commands/update.ts b/src/commands/update.ts index 33f9af29..9434d204 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -8,19 +8,53 @@ export default class UpdateCommand extends Command { static args = [{name: 'channel', optional: true}] + static 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: 'Update to a previously installed version.', + command: '<%= config.bin %> <%= command.id %> --version 1.0.0 --local', + }, + { + description: 'Interactively select version.', + command: '<%= config.bin %> <%= command.id %> --interactive', + }, + { + description: 'Interactively select a previously installed version.', + command: '<%= config.bin %> <%= command.id %> --interactive --local', + }, + { + description: 'Remove all existing versions and install stable version', + command: '<%= config.bin %> <%= command.id %> stable --hard', + }, + { + description: 'Remove all existing versions and install specific version', + command: '<%= config.bin %> <%= command.id %> --version 1.0.0 --hard', + }, + ] + static flags = { autoupdate: Flags.boolean({hidden: true}), - 'from-local': Flags.boolean({ - description: 'Interactively choose an already installed version. This is ignored if a channel is provided.', - exclusive: ['version', 'interactive'], + local: Flags.boolean({ + description: 'Switch to an already installed version. This is ignored if a channel is provided.', }), version: Flags.string({ description: 'Install a specific version.', - exclusive: ['from-local', 'interactive'], + exclusive: ['interactive'], }), interactive: Flags.boolean({ description: 'Interactively select version to install. This is ignored if a channel is provided.', - exclusive: ['from-local', 'version'], + exclusive: ['version'], + }), + hard: Flags.boolean({ + description: 'Remove all existing versions before updating to new version.', + exclusive: ['local'], }), } @@ -32,7 +66,7 @@ export default class UpdateCommand extends Command { } let version = flags.version - if (flags['from-local']) { + if (flags.interactive && flags.local) { version = await this.promptForLocalVersion() } else if (flags.interactive) { version = await this.promptForRemoteVersion() @@ -41,7 +75,8 @@ export default class UpdateCommand extends Command { const updateCli = new UpdateCli({ channel: args.channel, autoUpdate: flags.autoupdate, - fromLocal: flags['from-local'], + local: flags.local, + hard: flags.hard, version, config: this.config as Config, exit: this.exit, diff --git a/src/update.ts b/src/update.ts index 7325a1a4..b7a0ca7b 100644 --- a/src/update.ts +++ b/src/update.ts @@ -8,13 +8,14 @@ import * as _ from 'lodash' import * as path from 'path' import {extract} from './tar' -import {ls, wait} from './util' +import {ls, rm, wait} from './util' export interface UpdateCliOptions { channel?: string; autoUpdate: boolean; - fromLocal: boolean; + local: boolean; version: string | undefined; + hard: boolean; config: Config; exit: (code?: number | undefined) => void; } @@ -92,12 +93,12 @@ export default class UpdateCli { this.clientBin = UpdateCli.getClientBin(options.config) } - async runUpdate(): Promise { + public async runUpdate(): Promise { if (this.options.autoUpdate) await this.debounce() this.channel = this.options.channel || await this.determineChannel() - if (this.options.fromLocal) { + if (this.options.local) { await this.ensureClientDir() const version = this.options.version! if (!await fs.pathExists(path.join(this.clientRoot, version))) { @@ -111,6 +112,11 @@ export default class UpdateCli { CliUx.ux.log() 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}.`) } else if (this.options.version) { + if (this.options.hard) { + CliUx.ux.action.start(`${this.options.config.name}: Removing old installations`) + await rm(path.dirname(this.clientRoot)) + } + CliUx.ux.action.start(`${this.options.config.name}: Updating CLI`) await this.options.config.runHook('preupdate', {channel: this.channel, version: this.options.version}) @@ -134,6 +140,11 @@ export default class UpdateCli { CliUx.ux.log() 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 ${this.channel}.`) } else { + if (this.options.hard) { + CliUx.ux.action.start(`${this.options.config.name}: Removing old installations`) + await rm(path.dirname(this.clientRoot)) + } + CliUx.ux.action.start(`${this.options.config.name}: Updating CLI`) const manifest = await this.fetchChannelManifest() this.currentVersion = await this.determineCurrentVersion() @@ -323,7 +334,7 @@ export default class UpdateCli { if (output) { CliUx.ux.debug(msg) } else { - await CliUx.ux.log(msg) + CliUx.ux.log(msg) output = true } diff --git a/src/util.ts b/src/util.ts index c135c6a3..0d01db03 100644 --- a/src/util.ts +++ b/src/util.ts @@ -15,6 +15,13 @@ export async function ls(dir: string): Promise fs.stat(path).then(stat => ({path, stat})))) } +export async function rm(dir: string): Promise { + const files = await ls(dir) + for (const file of files) { + fs.rmSync(file.path, {recursive: true}) + } +} + export function wait(ms: number, unref = false): Promise { return new Promise(resolve => { const t: any = setTimeout(() => resolve(), ms) diff --git a/test/update.test.ts b/test/update.test.ts index 855c28c8..e22b4c11 100644 --- a/test/update.test.ts +++ b/test/update.test.ts @@ -34,10 +34,11 @@ function setupClientRoot(ctx: { config: IConfig }, createVersion?: string): stri function initUpdateCli(options: Partial): UpdateCli { const updateCli = new UpdateCli({channel: options.channel, - fromLocal: options.fromLocal || false, + local: options.local || false, autoUpdate: options.autoUpdate || false, config: options.config!, version: options.version, + hard: options.hard || false, exit: () => { // do nothing }, @@ -201,7 +202,7 @@ describe('update plugin', () => { 'Content-Encoding': 'gzip', }) - updateCli = initUpdateCli({fromLocal: true, config: config as Config, version: '2.0.0'}) + updateCli = initUpdateCli({local: true, config: config as Config, version: '2.0.0'}) await updateCli.runUpdate() const stdout = stripAnsi(collector.stdout.join(' ')) expect(stdout).to.matches(/Updating to an already installed version will not update the channel/) From 7234edfa31abf27210ed5e872ad9b2bfab848913 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Tue, 1 Feb 2022 13:09:24 -0700 Subject: [PATCH 06/17] chore: update examples --- src/commands/update.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/commands/update.ts b/src/commands/update.ts index 9434d204..0bf9bb20 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -10,31 +10,31 @@ export default class UpdateCommand extends Command { static examples = [ { - description: 'Update to the stable channel.', + description: 'Update to the stable channel:', command: '<%= config.bin %> <%= command.id %> stable', }, { - description: 'Update to a specific version.', + description: 'Update to a specific version:', command: '<%= config.bin %> <%= command.id %> --version 1.0.0', }, { - description: 'Update to a previously installed version.', + description: 'Update to a previously installed version:', command: '<%= config.bin %> <%= command.id %> --version 1.0.0 --local', }, { - description: 'Interactively select version.', + description: 'Interactively select version:', command: '<%= config.bin %> <%= command.id %> --interactive', }, { - description: 'Interactively select a previously installed version.', + description: 'Interactively select a previously installed version:', command: '<%= config.bin %> <%= command.id %> --interactive --local', }, { - description: 'Remove all existing versions and install stable version', + description: 'Remove all existing versions and install stable channel version:', command: '<%= config.bin %> <%= command.id %> stable --hard', }, { - description: 'Remove all existing versions and install specific version', + description: 'Remove all existing versions and install specific version:', command: '<%= config.bin %> <%= command.id %> --version 1.0.0 --hard', }, ] From 23a97a8c1c4c581f6354ce6097d0d941340f2ac6 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Tue, 1 Feb 2022 15:13:03 -0700 Subject: [PATCH 07/17] feat!: remove --local flag BREAKING CHANGE: remove --local flag --- src/commands/update.ts | 53 +++++++------------ src/update.ts | 117 ++++++++++++++++++++--------------------- test/update.test.ts | 11 ++-- 3 files changed, 79 insertions(+), 102 deletions(-) diff --git a/src/commands/update.ts b/src/commands/update.ts index 0bf9bb20..02ac4184 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -1,5 +1,6 @@ -import {Command, Flags, Config} from '@oclif/core' +import {Command, Flags, Config, CliUx} from '@oclif/core' import {prompt} from 'inquirer' +import * as path from 'path' import {sort} from 'semver' import UpdateCli from '../update' @@ -17,18 +18,10 @@ export default class UpdateCommand extends Command { description: 'Update to a specific version:', command: '<%= config.bin %> <%= command.id %> --version 1.0.0', }, - { - description: 'Update to a previously installed version:', - command: '<%= config.bin %> <%= command.id %> --version 1.0.0 --local', - }, { description: 'Interactively select version:', command: '<%= config.bin %> <%= command.id %> --interactive', }, - { - description: 'Interactively select a previously installed version:', - command: '<%= config.bin %> <%= command.id %> --interactive --local', - }, { description: 'Remove all existing versions and install stable channel version:', command: '<%= config.bin %> <%= command.id %> stable --hard', @@ -41,9 +34,7 @@ export default class UpdateCommand extends Command { static flags = { autoupdate: Flags.boolean({hidden: true}), - local: Flags.boolean({ - description: 'Switch to an already installed version. This is ignored if a channel is provided.', - }), + available: Flags.boolean({hidden: true}), version: Flags.string({ description: 'Install a specific version.', exclusive: ['interactive'], @@ -54,37 +45,42 @@ export default class UpdateCommand extends Command { }), hard: Flags.boolean({ description: 'Remove all existing versions before updating to new version.', - exclusive: ['local'], }), } async run(): Promise { const {args, flags} = await this.parse(UpdateCommand) - if (args.channel && flags.version) { - this.error('You cannot specifiy both a version and a channel.') + if (flags.available) { + const index = await UpdateCli.fetchVersionIndex(this.config) + const allVersions = sort(Object.keys(index)).reverse() + const localVersions = await UpdateCli.findLocalVersions(this.config) + + const table = allVersions.map(version => { + const location = localVersions.find(l => path.basename(l).startsWith(version)) || index[version] + return {version, location} + }) + + CliUx.ux.table(table, {version: {}, location: {}}) + return } - let version = flags.version - if (flags.interactive && flags.local) { - version = await this.promptForLocalVersion() - } else if (flags.interactive) { - version = await this.promptForRemoteVersion() + if (args.channel && flags.version) { + this.error('You cannot specifiy both a version and a channel.') } const updateCli = new UpdateCli({ channel: args.channel, autoUpdate: flags.autoupdate, - local: flags.local, hard: flags.hard, - version, + version: flags.interactive ? await this.promptForVersion() : flags.version, config: this.config as Config, exit: this.exit, }) return updateCli.runUpdate() } - private async promptForRemoteVersion(): Promise { + private async promptForVersion(): Promise { const choices = sort(Object.keys(await UpdateCli.fetchVersionIndex(this.config))).reverse() const {version} = await prompt<{version: string}>({ name: 'version', @@ -94,15 +90,4 @@ export default class UpdateCommand extends Command { }) return version } - - private async promptForLocalVersion(): Promise { - const choices = sort(UpdateCli.findLocalVersions(this.config)).reverse() - const {version} = await prompt<{version: string}>({ - name: 'version', - message: 'Select a version to update to', - type: 'list', - choices, - }) - return version - } } diff --git a/src/update.ts b/src/update.ts index b7a0ca7b..bc6d8628 100644 --- a/src/update.ts +++ b/src/update.ts @@ -13,7 +13,6 @@ import {ls, rm, wait} from './util' export interface UpdateCliOptions { channel?: string; autoUpdate: boolean; - local: boolean; version: string | undefined; hard: boolean; config: Config; @@ -39,11 +38,13 @@ export default class UpdateCli { private readonly clientBin: string - public static findLocalVersions(config: Config): string[] { + public static async findLocalVersions(config: Config): Promise { const clientRoot = UpdateCli.getClientRoot(config) - const versions = fs.readdirSync(clientRoot).filter(dirOrFile => dirOrFile !== 'bin' && dirOrFile !== 'current') - if (versions.length === 0) throw new Error('No locally installed versions found.') - return versions + await UpdateCli.ensureClientDir(clientRoot) + return fs + .readdirSync(clientRoot) + .filter(dirOrFile => dirOrFile !== 'bin' && dirOrFile !== 'current') + .map(f => path.join(clientRoot, f)) } public static async fetchVersionIndex(config: Config): Promise { @@ -62,6 +63,21 @@ export default class UpdateCli { return body } + private static async ensureClientDir(clientRoot: string): Promise { + try { + await fs.mkdirp(clientRoot) + } catch (error: any) { + if (error.code === 'EEXIST') { + // for some reason the client directory is sometimes a file + // if so, this happens. Delete it and recreate + await fs.remove(clientRoot) + await fs.mkdirp(clientRoot) + } else { + throw error + } + } + } + private static s3ChannelManifestKey(config: Config, channel: string): string { const {bin, platform, arch} = config const s3SubDir = composeS3SubDir(config) @@ -98,54 +114,43 @@ export default class UpdateCli { this.channel = this.options.channel || await this.determineChannel() - if (this.options.local) { - await this.ensureClientDir() - const version = this.options.version! - if (!await fs.pathExists(path.join(this.clientRoot, version))) { - throw new Error(`Version ${version} is not already installed at ${this.clientRoot}.`) - } - - CliUx.ux.action.start(`${this.options.config.name}: Updating CLI`) - CliUx.ux.debug(`switching to existing version ${version}`) - this.updateToExistingVersion(version) + if (this.options.hard) { + CliUx.ux.action.start(`${this.options.config.name}: Removing old installations`) + await rm(path.dirname(this.clientRoot)) + } - CliUx.ux.log() - 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}.`) - } else if (this.options.version) { - if (this.options.hard) { - CliUx.ux.action.start(`${this.options.config.name}: Removing old installations`) - await rm(path.dirname(this.clientRoot)) - } + CliUx.ux.action.start(`${this.options.config.name}: Updating CLI`) - CliUx.ux.action.start(`${this.options.config.name}: Updating CLI`) + if (this.options.version) { await this.options.config.runHook('preupdate', {channel: this.channel, version: this.options.version}) - const index = await UpdateCli.fetchVersionIndex(this.options.config) - const url = index[this.options.version] - if (!url) { - throw new Error(`${this.options.version} not found in index:\n${Object.keys(index).join(', ')}`) - } + const localVersion = await this.findLocalVersion(this.options.version) - const manifest = await this.fetchVersionManifest(this.options.version, url) - this.currentVersion = await this.determineCurrentVersion() - this.updatedVersion = manifest.sha ? `${manifest.version}-${manifest.sha}` : manifest.version - const reason = await this.skipUpdate() - if (reason) CliUx.ux.action.stop(reason || 'done') - else await this.update(manifest) + if (localVersion) { + this.updateToExistingVersion(localVersion) + } else { + const index = await UpdateCli.fetchVersionIndex(this.options.config) + const url = index[this.options.version] + if (!url) { + throw new Error(`${this.options.version} not found in index:\n${Object.keys(index).join(', ')}`) + } - CliUx.ux.debug('tidy') - await this.tidy() - await this.options.config.runHook('update', {channel: this.channel, version: this.updatedVersion}) + const manifest = await this.fetchVersionManifest(this.options.version, url) + this.currentVersion = await this.determineCurrentVersion() + this.updatedVersion = manifest.sha ? `${manifest.version}-${manifest.sha}` : manifest.version + const reason = await this.skipUpdate() + if (reason) CliUx.ux.action.stop(reason || 'done') + else await this.update(manifest) + + CliUx.ux.debug('tidy') + await this.tidy() + } + await this.options.config.runHook('update', {channel: this.channel, version: this.updatedVersion}) + CliUx.ux.action.stop() CliUx.ux.log() 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 ${this.channel}.`) } else { - if (this.options.hard) { - CliUx.ux.action.start(`${this.options.config.name}: Removing old installations`) - await rm(path.dirname(this.clientRoot)) - } - - CliUx.ux.action.start(`${this.options.config.name}: Updating CLI`) const manifest = await this.fetchChannelManifest() this.currentVersion = await this.determineCurrentVersion() this.updatedVersion = manifest.sha ? `${manifest.version}-${manifest.sha}` : manifest.version @@ -156,10 +161,10 @@ export default class UpdateCli { CliUx.ux.debug('tidy') await this.tidy() await this.options.config.runHook('update', {channel: this.channel, version: this.updatedVersion}) + CliUx.ux.action.stop() } CliUx.ux.debug('done') - CliUx.ux.action.stop() } private async fetchChannelManifest(): Promise { @@ -248,7 +253,7 @@ export default class UpdateCli { private async update(manifest: Interfaces.S3Manifest, channel = 'stable') { CliUx.ux.action.start(`${this.options.config.name}: Updating CLI from ${color.green(this.currentVersion)} to ${color.green(this.updatedVersion)}${channel === 'stable' ? '' : ' (' + color.yellow(channel) + ')'}`) - await this.ensureClientDir() + await UpdateCli.ensureClientDir(this.clientRoot) const output = path.join(this.clientRoot, this.updatedVersion) if (!await fs.pathExists(output)) { @@ -303,6 +308,13 @@ export default class UpdateCli { return this.options.config.version } + private async findLocalVersion(version: string): Promise { + const versions = await UpdateCli.findLocalVersions(this.options.config) + return versions + .map(file => path.basename(file)) + .find(file => file.startsWith(version)) + } + private async setChannel(): Promise { const channelPath = path.join(this.options.config.dataDir, 'channel') fs.writeFile(channelPath, this.channel, 'utf8') @@ -419,19 +431,4 @@ ${binPathEnvVar}="\$DIR/${bin}" ${redirectedEnvVar}=1 "$DIR/../${version}/bin/${ await fs.symlink(`./${version}`, path.join(this.clientRoot, 'current')) } } - - private async ensureClientDir(): Promise { - try { - await fs.mkdirp(this.clientRoot) - } catch (error: any) { - if (error.code === 'EEXIST') { - // for some reason the client directory is sometimes a file - // if so, this happens. Delete it and recreate - await fs.remove(this.clientRoot) - await fs.mkdirp(this.clientRoot) - } else { - throw error - } - } - } } diff --git a/test/update.test.ts b/test/update.test.ts index e22b4c11..753d31f7 100644 --- a/test/update.test.ts +++ b/test/update.test.ts @@ -34,7 +34,6 @@ function setupClientRoot(ctx: { config: IConfig }, createVersion?: string): stri function initUpdateCli(options: Partial): UpdateCli { const updateCli = new UpdateCli({channel: options.channel, - local: options.local || false, autoUpdate: options.autoUpdate || false, config: options.config!, version: options.version, @@ -127,11 +126,7 @@ describe('update plugin', () => { const versionManifestRegex = new RegExp(`example-cli-v2.0.1-${hash}-${config.platform}-${config.arch}-buildmanifest`) const tarballRegex = new RegExp(`tarballs\\/example-cli\\/example-cli-v2.0.1\\/example-cli-v2.0.1-${config.platform}-${config.arch}gz`) const indexRegex = new RegExp(`example-cli-${config.platform}-${config.arch}-tar-gz.json`) - const newVersionPath = path.join(clientRoot, '2.0.1') - // fs.mkdirpSync(path.join(newVersionPath, 'bin')) - fs.mkdirpSync(path.join(`${newVersionPath}.partial.11111`, 'bin')) - fs.writeFileSync(path.join(`${newVersionPath}.partial.11111`, 'bin', 'example-cli'), '../2.0.1/bin', 'utf8') - // fs.writeFileSync(path.join(newVersionPath, 'bin', 'example-cli'), '../2.0.1/bin', 'utf8') + sandbox.stub(extract, 'extract').resolves() sandbox.stub(zlib, 'gzipSync').returns(Buffer.alloc(1, ' ')) @@ -202,9 +197,9 @@ describe('update plugin', () => { 'Content-Encoding': 'gzip', }) - updateCli = initUpdateCli({local: true, config: config as Config, version: '2.0.0'}) + updateCli = initUpdateCli({config: config as Config, version: '2.0.0'}) await updateCli.runUpdate() const stdout = stripAnsi(collector.stdout.join(' ')) - expect(stdout).to.matches(/Updating to an already installed version will not update the channel/) + expect(stdout).to.matches(/Updating to a specific version will not update the channel/) }) }) From 23e5532be9f7fb4b4e9bb2b3d903793f3cabb961 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Tue, 1 Feb 2022 19:23:51 -0700 Subject: [PATCH 08/17] refactor: Updater --- bin/dev | 3 +- package.json | 4 +- src/commands/update.ts | 21 ++- src/hooks/init.ts | 2 +- src/update.ts | 336 +++++++++++++++++++---------------------- src/util.ts | 10 +- test/update.test.ts | 56 ++++--- tsconfig.json | 3 +- yarn.lock | 14 +- 9 files changed, 216 insertions(+), 233 deletions(-) diff --git a/bin/dev b/bin/dev index 5a42fcac..bbc3f51d 100755 --- a/bin/dev +++ b/bin/dev @@ -11,8 +11,7 @@ process.env.NODE_ENV = 'development' require('ts-node').register({project}) // In dev mode, always show stack traces -// Waiting for https://github.com/oclif/core/pull/147 -// oclif.settings.debug = true; +oclif.settings.debug = true; // Start the CLI oclif.run().then(oclif.flush).catch(oclif.Errors.handle) diff --git a/package.json b/package.json index 8ee505ef..aff8f37b 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "fs-extra": "^9.0.1", "http-call": "^5.3.0", "inquirer": "^8.2.0", - "lodash": "^4.17.21", + "lodash.throttle": "^4.1.1", "log-chopper": "^1.0.2", "semver": "^7.3.5", "tar-fs": "^2.1.1" @@ -26,7 +26,7 @@ "@types/fs-extra": "^8.0.1", "@types/glob": "^7.1.3", "@types/inquirer": "^8.2.0", - "@types/lodash": "^4.14.168", + "@types/lodash.throttle": "^4.1.6", "@types/mocha": "^9", "@types/node": "^14.14.31", "@types/semver": "^7.3.4", diff --git a/src/commands/update.ts b/src/commands/update.ts index 02ac4184..b6984c98 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -1,8 +1,8 @@ -import {Command, Flags, Config, CliUx} from '@oclif/core' +import {Command, Flags, CliUx} from '@oclif/core' import {prompt} from 'inquirer' import * as path from 'path' import {sort} from 'semver' -import UpdateCli from '../update' +import {Updater} from '../update' export default class UpdateCommand extends Command { static description = 'update the <%= config.bin %> CLI' @@ -50,11 +50,11 @@ export default class UpdateCommand extends Command { async run(): Promise { const {args, flags} = await this.parse(UpdateCommand) - + const updater = new Updater(this.config) if (flags.available) { - const index = await UpdateCli.fetchVersionIndex(this.config) + const index = await updater.fetchVersionIndex() const allVersions = sort(Object.keys(index)).reverse() - const localVersions = await UpdateCli.findLocalVersions(this.config) + const localVersions = await updater.findLocalVersions() const table = allVersions.map(version => { const location = localVersions.find(l => path.basename(l).startsWith(version)) || index[version] @@ -69,19 +69,16 @@ export default class UpdateCommand extends Command { this.error('You cannot specifiy both a version and a channel.') } - const updateCli = new UpdateCli({ + return updater.runUpdate({ channel: args.channel, autoUpdate: flags.autoupdate, hard: flags.hard, - version: flags.interactive ? await this.promptForVersion() : flags.version, - config: this.config as Config, - exit: this.exit, + version: flags.interactive ? await this.promptForVersion(updater) : flags.version, }) - return updateCli.runUpdate() } - private async promptForVersion(): Promise { - const choices = sort(Object.keys(await UpdateCli.fetchVersionIndex(this.config))).reverse() + private async promptForVersion(updater: Updater): Promise { + const choices = sort(Object.keys(await updater.fetchVersionIndex())).reverse() const {version} = await prompt<{version: string}>({ name: 'version', message: 'Select a version to update to', diff --git a/src/hooks/init.ts b/src/hooks/init.ts index 8c2db8a4..cbfa3ef8 100644 --- a/src/hooks/init.ts +++ b/src/hooks/init.ts @@ -1,5 +1,5 @@ import {CliUx, Interfaces} from '@oclif/core' -import * as spawn from 'cross-spawn' +import spawn from 'cross-spawn' import * as fs from 'fs-extra' import * as path from 'path' diff --git a/src/update.ts b/src/update.ts index bc6d8628..62642158 100644 --- a/src/update.ts +++ b/src/update.ts @@ -4,58 +4,106 @@ import {Config, CliUx, Interfaces} from '@oclif/core' import * as fs from 'fs-extra' import HTTP from 'http-call' -import * as _ from 'lodash' import * as path from 'path' +import throttle from 'lodash.throttle' +import fileSize from 'filesize' import {extract} from './tar' import {ls, rm, wait} from './util' -export interface UpdateCliOptions { - channel?: string; - autoUpdate: boolean; - version: string | undefined; - hard: boolean; - config: Config; - exit: (code?: number | undefined) => void; +const filesize = (n: number): string => { + const [num, suffix] = fileSize(n, {output: 'array'}) + return Number.parseFloat(num).toFixed(1) + ` ${suffix}` } -export type VersionIndex = Record +export namespace Updater { + export type Options = { + channel?: string | undefined; + autoUpdate: boolean; + version?: string | undefined + hard: boolean; + } -function composeS3SubDir(config: Config): string { - let s3SubDir = (config.pjson.oclif.update.s3 as any).folder || '' - if (s3SubDir !== '' && s3SubDir.slice(-1) !== '/') s3SubDir = `${s3SubDir}/` - return s3SubDir + export type VersionIndex = Record } -export default class UpdateCli { - private channel!: string +export class Updater { + private readonly clientRoot: string + private readonly clientBin: string - private currentVersion?: string + constructor(private config: Config) { + this.clientRoot = config.scopedEnvVar('OCLIF_CLIENT_HOME') || path.join(config.dataDir, 'client') + this.clientBin = path.join(this.clientRoot, 'bin', config.windows ? `${config.bin}.cmd` : config.bin) + } - private updatedVersion!: string + public async runUpdate(options: Updater.Options): Promise { + const {autoUpdate, version, hard} = options + if (autoUpdate) await this.debounce() - private readonly clientRoot: string + if (hard) { + CliUx.ux.action.start(`${this.config.name}: Removing old installations`) + await rm(path.dirname(this.clientRoot)) + } - private readonly clientBin: string + const channel = options.channel || await this.determineChannel() + const current = await this.determineCurrentVersion() + + CliUx.ux.action.start(`${this.config.name}: Updating CLI`) + + if (version) { + await this.config.runHook('preupdate', {channel, version}) + + const localVersion = await this.findLocalVersion(version) - public static async findLocalVersions(config: Config): Promise { - const clientRoot = UpdateCli.getClientRoot(config) - await UpdateCli.ensureClientDir(clientRoot) + if (localVersion) { + this.updateToExistingVersion(current, localVersion) + } else { + const index = await this.fetchVersionIndex() + const url = index[version] + if (!url) { + throw new Error(`${version} not found in index:\n${Object.keys(index).join(', ')}`) + } + + const manifest = await this.fetchVersionManifest(version, url) + const updated = manifest.sha ? `${manifest.version}-${manifest.sha}` : manifest.version + const reason = await this.skipUpdate(current, updated) + if (reason) CliUx.ux.action.stop(reason || 'done') + else await this.update(manifest, current, updated) + await this.tidy() + } + + await this.config.runHook('update', {channel, version}) + CliUx.ux.action.stop() + CliUx.ux.log() + 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}.`) + } else { + const manifest = await this.fetchChannelManifest(channel) + const updated = manifest.sha ? `${manifest.version}-${manifest.sha}` : manifest.version + await this.config.runHook('preupdate', {channel, version: updated}) + const reason = await this.skipUpdate(current, updated) + if (reason) CliUx.ux.action.stop(reason || 'done') + else await this.update(manifest, current, updated, channel) + await this.tidy() + await this.config.runHook('update', {channel, version: updated}) + CliUx.ux.action.stop() + } + + CliUx.ux.debug('done') + } + + public async findLocalVersions(): Promise { + await this.ensureClientDir() return fs - .readdirSync(clientRoot) + .readdirSync(this.clientRoot) .filter(dirOrFile => dirOrFile !== 'bin' && dirOrFile !== 'current') - .map(f => path.join(clientRoot, f)) + .map(f => path.join(this.clientRoot, f)) } - public static async fetchVersionIndex(config: Config): Promise { - const http: typeof HTTP = require('http-call').HTTP - + public async fetchVersionIndex(): Promise { CliUx.ux.action.status = 'fetching version index' - const newIndexUrl = config.s3Url( - UpdateCli.s3VersionIndexKey(config), - ) + const newIndexUrl = this.config.s3Url(this.s3VersionIndexKey()) - const {body} = await http.get(newIndexUrl) + const {body} = await HTTP.get(newIndexUrl) if (typeof body === 'string') { return JSON.parse(body) } @@ -63,177 +111,102 @@ export default class UpdateCli { return body } - private static async ensureClientDir(clientRoot: string): Promise { + private async ensureClientDir(): Promise { try { - await fs.mkdirp(clientRoot) + await fs.mkdirp(this.clientRoot) } catch (error: any) { if (error.code === 'EEXIST') { // for some reason the client directory is sometimes a file // if so, this happens. Delete it and recreate - await fs.remove(clientRoot) - await fs.mkdirp(clientRoot) + await fs.remove(this.clientRoot) + await fs.mkdirp(this.clientRoot) } else { throw error } } } - private static s3ChannelManifestKey(config: Config, channel: string): string { - const {bin, platform, arch} = config - const s3SubDir = composeS3SubDir(config) + private composeS3SubDir(): string { + let s3SubDir = (this.config.pjson.oclif.update.s3 as any).folder || '' + if (s3SubDir !== '' && s3SubDir.slice(-1) !== '/') s3SubDir = `${s3SubDir}/` + return s3SubDir + } + + private s3ChannelManifestKey(channel: string): string { + const {bin, platform, arch} = this.config + const s3SubDir = this.composeS3SubDir() return path.join(s3SubDir, 'channels', channel, `${bin}-${platform}-${arch}-buildmanifest`) } - private static s3VersionManifestKey(config: Config, version: string, hash: string): string { - const {bin, platform, arch} = config - const s3SubDir = composeS3SubDir(config) + private s3VersionManifestKey(version: string, hash: string): string { + const {bin, platform, arch} = this.config + const s3SubDir = this.composeS3SubDir() return path.join(s3SubDir, 'versions', version, hash, `${bin}-v${version}-${hash}-${platform}-${arch}-buildmanifest`) } - private static s3VersionIndexKey(config: Config): string { - const {bin, platform, arch} = config - const s3SubDir = composeS3SubDir(config) + private s3VersionIndexKey(): string { + const {bin, platform, arch} = this.config + const s3SubDir = this.composeS3SubDir() return path.join(s3SubDir, 'versions', `${bin}-${platform}-${arch}-tar-gz.json`) } - private static getClientRoot(config: Config): string { - return config.scopedEnvVar('OCLIF_CLIENT_HOME') || path.join(config.dataDir, 'client') - } - - private static getClientBin(config: Config): string { - return path.join(UpdateCli.getClientRoot(config), 'bin', config.windows ? `${config.bin}.cmd` : config.bin) - } - - constructor(private options: UpdateCliOptions) { - this.clientRoot = UpdateCli.getClientRoot(options.config) - this.clientBin = UpdateCli.getClientBin(options.config) - } - - public async runUpdate(): Promise { - if (this.options.autoUpdate) await this.debounce() - - this.channel = this.options.channel || await this.determineChannel() - - if (this.options.hard) { - CliUx.ux.action.start(`${this.options.config.name}: Removing old installations`) - await rm(path.dirname(this.clientRoot)) - } - - CliUx.ux.action.start(`${this.options.config.name}: Updating CLI`) - - if (this.options.version) { - await this.options.config.runHook('preupdate', {channel: this.channel, version: this.options.version}) - - const localVersion = await this.findLocalVersion(this.options.version) - - if (localVersion) { - this.updateToExistingVersion(localVersion) - } else { - const index = await UpdateCli.fetchVersionIndex(this.options.config) - const url = index[this.options.version] - if (!url) { - throw new Error(`${this.options.version} not found in index:\n${Object.keys(index).join(', ')}`) - } - - const manifest = await this.fetchVersionManifest(this.options.version, url) - this.currentVersion = await this.determineCurrentVersion() - this.updatedVersion = manifest.sha ? `${manifest.version}-${manifest.sha}` : manifest.version - const reason = await this.skipUpdate() - if (reason) CliUx.ux.action.stop(reason || 'done') - else await this.update(manifest) - - CliUx.ux.debug('tidy') - await this.tidy() - } - - await this.options.config.runHook('update', {channel: this.channel, version: this.updatedVersion}) - CliUx.ux.action.stop() - CliUx.ux.log() - 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 ${this.channel}.`) - } else { - const manifest = await this.fetchChannelManifest() - this.currentVersion = await this.determineCurrentVersion() - this.updatedVersion = manifest.sha ? `${manifest.version}-${manifest.sha}` : manifest.version - await this.options.config.runHook('preupdate', {channel: this.channel, version: this.updatedVersion}) - const reason = await this.skipUpdate() - if (reason) CliUx.ux.action.stop(reason || 'done') - else await this.update(manifest) - CliUx.ux.debug('tidy') - await this.tidy() - await this.options.config.runHook('update', {channel: this.channel, version: this.updatedVersion}) - CliUx.ux.action.stop() + private async fetchChannelManifest(channel: string): Promise { + const s3Key = this.s3ChannelManifestKey(channel) + try { + return await this.fetchManifest(s3Key) + } catch (error: any) { + if (error.statusCode === 403) throw new Error(`HTTP 403: Invalid channel ${channel}`) + throw error } - - CliUx.ux.debug('done') - } - - private async fetchChannelManifest(): Promise { - const s3Key = UpdateCli.s3ChannelManifestKey(this.options.config, this.channel) - return this.fetchManifest(s3Key) } private async fetchVersionManifest(version: string, url: string): Promise { const parts = url.split('/') const hashIndex = parts.indexOf(version) + 1 const hash = parts[hashIndex] - const s3Key = UpdateCli.s3VersionManifestKey(this.options.config, version, hash) + const s3Key = this.s3VersionManifestKey(version, hash) return this.fetchManifest(s3Key) } private async fetchManifest(s3Key: string): Promise { - const http: typeof HTTP = require('http-call').HTTP - CliUx.ux.action.status = 'fetching manifest' - try { - const url = this.options.config.s3Url(s3Key) - const {body} = await http.get(url) - if (typeof body === 'string') { - return JSON.parse(body) - } - - return body - } catch (error: any) { - if (error.statusCode === 403) throw new Error(`HTTP 403: Invalid channel ${this.channel}`) - throw error + const url = this.config.s3Url(s3Key) + const {body} = await HTTP.get(url) + if (typeof body === 'string') { + return JSON.parse(body) } + + return body } private async downloadAndExtract(output: string, manifest: Interfaces.S3Manifest, channel: string) { const {version, gz, sha256gz} = manifest - const filesize = (n: number): string => { - const [num, suffix] = require('filesize')(n, {output: 'array'}) - return num.toFixed(1) + ` ${suffix}` - } - - const http: typeof HTTP = require('http-call').HTTP - const gzUrl = gz || this.options.config.s3Url(this.options.config.s3Key('versioned', { + const gzUrl = gz || this.config.s3Url(this.config.s3Key('versioned', { version, channel, - bin: this.options.config.bin, - platform: this.options.config.platform, - arch: this.options.config.arch, + bin: this.config.bin, + platform: this.config.platform, + arch: this.config.arch, ext: 'gz', })) - const {response: stream} = await http.stream(gzUrl) + const {response: stream} = await HTTP.stream(gzUrl) stream.pause() - const baseDir = manifest.baseDir || this.options.config.s3Key('baseDir', { + const baseDir = manifest.baseDir || this.config.s3Key('baseDir', { version, channel, - bin: this.options.config.bin, - platform: this.options.config.platform, - arch: this.options.config.arch, + bin: this.config.bin, + platform: this.config.platform, + arch: this.config.arch, }) const extraction = extract(stream, baseDir, output, sha256gz) - // to-do: use cli.action.type - if ((CliUx.ux.action as any).frames) { - // if spinner action + if (CliUx.ux.action.type === 'spinner') { const total = Number.parseInt(stream.headers['content-length']!, 10) let current = 0 - const updateStatus = _.throttle( + const updateStatus = throttle( (newStatus: string) => { CliUx.ux.action.status = newStatus }, @@ -250,81 +223,83 @@ export default class UpdateCli { await extraction } - private async update(manifest: Interfaces.S3Manifest, channel = 'stable') { - CliUx.ux.action.start(`${this.options.config.name}: Updating CLI from ${color.green(this.currentVersion)} to ${color.green(this.updatedVersion)}${channel === 'stable' ? '' : ' (' + color.yellow(channel) + ')'}`) + private async update(manifest: Interfaces.S3Manifest, current: string, updated: string, channel = 'stable') { + CliUx.ux.action.start(`${this.config.name}: Updating CLI from ${color.green(current)} to ${color.green(updated)}${channel === 'stable' ? '' : ' (' + color.yellow(channel) + ')'}`) - await UpdateCli.ensureClientDir(this.clientRoot) - const output = path.join(this.clientRoot, this.updatedVersion) + await this.ensureClientDir() + const output = path.join(this.clientRoot, updated) if (!await fs.pathExists(output)) { await this.downloadAndExtract(output, manifest, channel) } - await this.setChannel() - await this.createBin(this.updatedVersion) + await this.setChannel(channel) + await this.createBin(updated) await this.touch() CliUx.ux.action.stop() } - private async updateToExistingVersion(version: string): Promise { - await this.createBin(version) + private async updateToExistingVersion(current: string, updated: string): Promise { + CliUx.ux.action.start(`${this.config.name}: Updating CLI from ${color.green(current)} to ${color.green(updated)}`) + await this.createBin(updated) await this.touch() + CliUx.ux.action.stop() } - private async skipUpdate(): Promise { - if (!this.options.config.binPath) { - const instructions = this.options.config.scopedEnvVar('UPDATE_INSTRUCTIONS') + private async skipUpdate(current: string, updated: string): Promise { + if (!this.config.binPath) { + const instructions = this.config.scopedEnvVar('UPDATE_INSTRUCTIONS') if (instructions) CliUx.ux.warn(instructions) return 'not updatable' } - if (this.currentVersion === this.updatedVersion) { - if (this.options.config.scopedEnvVar('HIDE_UPDATED_MESSAGE')) return 'done' - return `already on latest version: ${this.currentVersion}` + if (current === updated) { + if (this.config.scopedEnvVar('HIDE_UPDATED_MESSAGE')) return 'done' + return `already on latest version: ${current}` } return false } private async determineChannel(): Promise { - const channelPath = path.join(this.options.config.dataDir, 'channel') + const channelPath = path.join(this.config.dataDir, 'channel') if (fs.existsSync(channelPath)) { const channel = await fs.readFile(channelPath, 'utf8') return String(channel).trim() } - return this.options.config.channel || 'stable' + return this.config.channel || 'stable' } - private async determineCurrentVersion(): Promise { + private async determineCurrentVersion(): Promise { try { const currentVersion = await fs.readFile(this.clientBin, 'utf8') const matches = currentVersion.match(/\.\.[/\\|](.+)[/\\|]bin/) - return matches ? matches[1] : this.options.config.version + return matches ? matches[1] : this.config.version } catch (error: any) { CliUx.ux.debug(error) } - return this.options.config.version + return this.config.version } private async findLocalVersion(version: string): Promise { - const versions = await UpdateCli.findLocalVersions(this.options.config) + const versions = await this.findLocalVersions() return versions .map(file => path.basename(file)) .find(file => file.startsWith(version)) } - private async setChannel(): Promise { - const channelPath = path.join(this.options.config.dataDir, 'channel') - fs.writeFile(channelPath, this.channel, 'utf8') + private async setChannel(channel: string): Promise { + const channelPath = path.join(this.config.dataDir, 'channel') + fs.writeFile(channelPath, channel, 'utf8') } private async logChop(): Promise { try { CliUx.ux.debug('log chop') const logChopper = require('log-chopper').default - await logChopper.chop(this.options.config.errlog) + await logChopper.chop(this.config.errlog) } catch (error: any) { CliUx.ux.debug(error.message) } @@ -338,7 +313,7 @@ export default class UpdateCli { // when autoupdating, wait until the CLI isn't active private async debounce(): Promise { let output = false - const lastrunfile = path.join(this.options.config.cacheDir, 'lastrun') + const lastrunfile = path.join(this.config.cacheDir, 'lastrun') const m = await this.mtime(lastrunfile) m.setHours(m.getHours() + 1) if (m > new Date()) { @@ -359,12 +334,13 @@ export default class UpdateCli { // removes any unused CLIs private async tidy(): Promise { + CliUx.ux.debug('tidy') try { const root = this.clientRoot if (!await fs.pathExists(root)) return const files = await ls(root) const promises = files.map(async (f: any) => { - if (['bin', 'current', this.options.config.version].includes(path.basename(f.path))) return + if (['bin', 'current', this.config.version].includes(path.basename(f.path))) return const mtime = f.stat.mtime mtime.setHours(mtime.getHours() + (42 * 24)) if (mtime < new Date()) { @@ -381,7 +357,7 @@ export default class UpdateCli { private async touch(): Promise { // touch the client so it won't be tidied up right away try { - const p = path.join(this.clientRoot, this.options.config.version) + const p = path.join(this.clientRoot, this.config.version) CliUx.ux.debug('touching client at', p) if (!await fs.pathExists(p)) return await fs.utimes(p, new Date(), new Date()) @@ -392,9 +368,9 @@ export default class UpdateCli { private async createBin(version: string): Promise { const dst = this.clientBin - const {bin, windows} = this.options.config - const binPathEnvVar = this.options.config.scopedEnvVarKey('BINPATH') - const redirectedEnvVar = this.options.config.scopedEnvVarKey('REDIRECTED') + const {bin, windows} = this.config + const binPathEnvVar = this.config.scopedEnvVarKey('BINPATH') + const redirectedEnvVar = this.config.scopedEnvVarKey('REDIRECTED') if (windows) { const body = `@echo off setlocal enableextensions diff --git a/src/util.ts b/src/util.ts index 0d01db03..99cc5513 100644 --- a/src/util.ts +++ b/src/util.ts @@ -16,10 +16,12 @@ export async function ls(dir: string): Promise { - const files = await ls(dir) - for (const file of files) { - fs.rmSync(file.path, {recursive: true}) - } + return new Promise((resolve, reject) => { + fs.rm(dir, {recursive: true}, (err: Error | null) => { + if (err) reject(err) + resolve() + }) + }) } export function wait(ms: number, unref = false): Promise { diff --git a/test/update.test.ts b/test/update.test.ts index 753d31f7..656b2e72 100644 --- a/test/update.test.ts +++ b/test/update.test.ts @@ -2,9 +2,9 @@ import * as fs from 'fs-extra' import * as path from 'path' import {Config, CliUx} from '@oclif/core' import {Config as IConfig} from '@oclif/core/lib/interfaces' -import UpdateCli, {UpdateCliOptions} from '../src/update' +import {Updater} from '../src/update' import * as zlib from 'zlib' -import * as nock from 'nock' +import nock from 'nock' import * as sinon from 'sinon' import stripAnsi = require('strip-ansi') import * as extract from '../src/tar' @@ -32,29 +32,21 @@ function setupClientRoot(ctx: { config: IConfig }, createVersion?: string): stri return clientRoot } -function initUpdateCli(options: Partial): UpdateCli { - const updateCli = new UpdateCli({channel: options.channel, - autoUpdate: options.autoUpdate || false, - config: options.config!, - version: options.version, - hard: options.hard || false, - exit: () => { - // do nothing - }, - }) - expect(updateCli).to.be.ok - return updateCli +function initUpdater(config: Config): Updater { + const updater = new Updater(config) + expect(updater).to.be.ok + return updater } describe('update plugin', () => { - let config: IConfig - let updateCli: UpdateCli + let config: Config + let updater: Updater let collector: OutputCollectors let clientRoot: string let sandbox: sinon.SinonSandbox beforeEach(async () => { - config = await loadConfig({root: path.join(process.cwd(), 'examples', 's3-update-example-cli')}) + config = await loadConfig({root: path.join(process.cwd(), 'examples', 's3-update-example-cli')}) as Config config.binPath = config.binPath || config.bin collector = {stdout: [], stderr: []} sandbox = sinon.createSandbox() @@ -63,6 +55,7 @@ describe('update plugin', () => { sandbox.stub(CliUx.ux.action, 'start').callsFake(line => collector.stdout.push(line || '')) sandbox.stub(CliUx.ux.action, 'stop').callsFake(line => collector.stdout.push(line || '')) }) + afterEach(() => { nock.cleanAll() if (fs.pathExistsSync(clientRoot)) { @@ -71,6 +64,7 @@ describe('update plugin', () => { sandbox.restore() }) + it('should not update - already on same version', async () => { clientRoot = setupClientRoot({config}, '2.0.0') const platformRegex = new RegExp(`tarballs\\/example-cli\\/${config.platform}-${config.arch}`) @@ -81,21 +75,20 @@ describe('update plugin', () => { .get(manifestRegex) .reply(200, {version: '2.0.0'}) - updateCli = initUpdateCli({config: config! as Config}) - await updateCli.runUpdate() + updater = initUpdater(config) + await updater.runUpdate({autoUpdate: false, hard: false}) const stdout = collector.stdout.join(' ') expect(stdout).to.include('already on latest version') }) + it('should update to channel', async () => { clientRoot = setupClientRoot({config}) const platformRegex = new RegExp(`tarballs\\/example-cli\\/${config.platform}-${config.arch}`) const manifestRegex = new RegExp(`channels\\/stable\\/example-cli-${config.platform}-${config.arch}-buildmanifest`) const tarballRegex = new RegExp(`tarballs\\/example-cli\\/example-cli-v2.0.1\\/example-cli-v2.0.1-${config.platform}-${config.arch}gz`) const newVersionPath = path.join(clientRoot, '2.0.1') - // fs.mkdirpSync(path.join(newVersionPath, 'bin')) fs.mkdirpSync(path.join(`${newVersionPath}.partial.11111`, 'bin')) fs.writeFileSync(path.join(`${newVersionPath}.partial.11111`, 'bin', 'example-cli'), '../2.0.1/bin', 'utf8') - // fs.writeFileSync(path.join(newVersionPath, 'bin', 'example-cli'), '../2.0.1/bin', 'utf8') sandbox.stub(extract, 'extract').resolves() sandbox.stub(zlib, 'gzipSync').returns(Buffer.alloc(1, ' ')) @@ -113,11 +106,12 @@ describe('update plugin', () => { 'Content-Encoding': 'gzip', }) - updateCli = initUpdateCli({config: config as Config}) - await updateCli.runUpdate() + updater = initUpdater(config) + await updater.runUpdate({autoUpdate: false, hard: false}) const stdout = stripAnsi(collector.stdout.join(' ')) expect(stdout).to.matches(/Updating CLI from 2.0.0 to 2.0.1/) }) + it('should update to version', async () => { const hash = 'f289627' clientRoot = setupClientRoot({config}) @@ -150,11 +144,12 @@ describe('update plugin', () => { '2.0.1': `versions/example-cli/2.0.1/${hash}/example-cli-v2.0.1-${config.platform}-${config.arch}.gz`, }) - updateCli = initUpdateCli({config: config as Config, version: '2.0.1'}) - await updateCli.runUpdate() + updater = initUpdater(config) + await updater.runUpdate({autoUpdate: false, hard: false, version: '2.0.1'}) const stdout = stripAnsi(collector.stdout.join(' ')) expect(stdout).to.matches(/Updating CLI from 2.0.0 to 2.0.1/) }) + it('should not update - not updatable', async () => { clientRoot = setupClientRoot({config}) // unset binPath @@ -165,16 +160,17 @@ describe('update plugin', () => { .get(/channels\/stable\/example-cli-.+?-buildmanifest/) .reply(200, {version: '2.0.0'}) - updateCli = initUpdateCli({config: config as Config}) - await updateCli.runUpdate() + updater = initUpdater(config) + await updater.runUpdate({autoUpdate: false, hard: false}) const stdout = collector.stdout.join(' ') expect(stdout).to.include('not updatable') }) + it('should update from local file', async () => { clientRoot = setupClientRoot({config}) const platformRegex = new RegExp(`tarballs\\/example-cli\\/${config.platform}-${config.arch}`) const manifestRegex = new RegExp(`channels\\/stable\\/example-cli-${config.platform}-${config.arch}-buildmanifest`) - const tarballRegex = new RegExp(`tarballs\\/example-cli\\/example-cli-v2.0.1\\/example-cli-v2.0.0-${config.platform}-${config.arch}gz`) + const tarballRegex = new RegExp(`tarballs\\/example-cli\\/example-cli-v2.0.0\\/example-cli-v2.0.0-${config.platform}-${config.arch}gz`) const newVersionPath = path.join(clientRoot, '2.0.0') fs.mkdirpSync(path.join(newVersionPath, 'bin')) fs.mkdirpSync(path.join(`${newVersionPath}.partial.11111`, 'bin')) @@ -197,8 +193,8 @@ describe('update plugin', () => { 'Content-Encoding': 'gzip', }) - updateCli = initUpdateCli({config: config as Config, version: '2.0.0'}) - await updateCli.runUpdate() + updater = initUpdater(config) + await updater.runUpdate({autoUpdate: false, hard: false, version: '2.0.0'}) const stdout = stripAnsi(collector.stdout.join(' ')) expect(stdout).to.matches(/Updating to a specific version will not update the channel/) }) diff --git a/tsconfig.json b/tsconfig.json index c8a673e2..8c8b594d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,8 @@ "./src" ], "strict": true, - "target": "es2017" + "target": "es2017", + "esModuleInterop": true }, "include": [ "./src/**/*" diff --git a/yarn.lock b/yarn.lock index 4b529a0a..8f04a681 100644 --- a/yarn.lock +++ b/yarn.lock @@ -745,7 +745,14 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== -"@types/lodash@*", "@types/lodash@^4.14.168": +"@types/lodash.throttle@^4.1.6": + version "4.1.6" + resolved "https://registry.npmjs.org/@types/lodash.throttle/-/lodash.throttle-4.1.6.tgz#f5ba2c22244ee42ff6c2c49e614401a870c1009c" + integrity sha512-/UIH96i/sIRYGC60NoY72jGkCJtFN5KVPhEMMMTjol65effe1gPn0tycJqV5tlSwMTzX8FqzB5yAj0rfGHTPNg== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": version "4.14.168" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.168.tgz#fe24632e79b7ade3f132891afff86caa5e5ce008" integrity sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q== @@ -3405,6 +3412,11 @@ lodash.set@^4.3.2: resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= +lodash.throttle@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" + integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ= + lodash.truncate@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" From f840578e3bf2bb35b5919a8639e4bad293241213 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 2 Feb 2022 08:42:25 -0700 Subject: [PATCH 09/17] fix: handle non-existent version index --- src/update.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/update.ts b/src/update.ts index 62642158..f1a4e0bd 100644 --- a/src/update.ts +++ b/src/update.ts @@ -102,13 +102,16 @@ export class Updater { public async fetchVersionIndex(): Promise { CliUx.ux.action.status = 'fetching version index' const newIndexUrl = this.config.s3Url(this.s3VersionIndexKey()) + try { + const {body} = await HTTP.get(newIndexUrl) + if (typeof body === 'string') { + return JSON.parse(body) + } - const {body} = await HTTP.get(newIndexUrl) - if (typeof body === 'string') { - return JSON.parse(body) + return body + } catch { + throw new Error(`No version indices exist for ${this.config.name}.`) } - - return body } private async ensureClientDir(): Promise { From 41a57f8abcbe5d4bd8f8efe547f150a1426a0b67 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Mon, 7 Feb 2022 09:31:14 -0700 Subject: [PATCH 10/17] chore: code review --- src/update.ts | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/update.ts b/src/update.ts index f1a4e0bd..0a701a27 100644 --- a/src/update.ts +++ b/src/update.ts @@ -40,6 +40,13 @@ export class Updater { const {autoUpdate, version, hard} = options if (autoUpdate) await this.debounce() + CliUx.ux.action.start(`${this.config.name}: Updating CLI`) + + if (this.notUpdatable()) { + CliUx.ux.action.stop('not updatable') + return + } + if (hard) { CliUx.ux.action.start(`${this.config.name}: Removing old installations`) await rm(path.dirname(this.clientRoot)) @@ -48,14 +55,14 @@ export class Updater { const channel = options.channel || await this.determineChannel() const current = await this.determineCurrentVersion() - CliUx.ux.action.start(`${this.config.name}: Updating CLI`) - if (version) { await this.config.runHook('preupdate', {channel, version}) const localVersion = await this.findLocalVersion(version) - if (localVersion) { + if (this.alreadyOnVersion(current, localVersion || null)) { + CliUx.ux.action.stop(this.config.scopedEnvVar('HIDE_UPDATED_MESSAGE') ? 'done' : `already on version ${current}`) + } else if (localVersion) { this.updateToExistingVersion(current, localVersion) } else { const index = await this.fetchVersionIndex() @@ -66,9 +73,7 @@ export class Updater { const manifest = await this.fetchVersionManifest(version, url) const updated = manifest.sha ? `${manifest.version}-${manifest.sha}` : manifest.version - const reason = await this.skipUpdate(current, updated) - if (reason) CliUx.ux.action.stop(reason || 'done') - else await this.update(manifest, current, updated) + await this.update(manifest, current, updated) await this.tidy() } @@ -80,10 +85,14 @@ export class Updater { const manifest = await this.fetchChannelManifest(channel) const updated = manifest.sha ? `${manifest.version}-${manifest.sha}` : manifest.version await this.config.runHook('preupdate', {channel, version: updated}) - const reason = await this.skipUpdate(current, updated) - if (reason) CliUx.ux.action.stop(reason || 'done') - else await this.update(manifest, current, updated, channel) - await this.tidy() + + if (!hard && this.alreadyOnVersion(current, updated)) { + CliUx.ux.action.stop(this.config.scopedEnvVar('HIDE_UPDATED_MESSAGE') ? 'done' : `already on version ${current}`) + } else { + await this.update(manifest, current, updated, channel) + await this.tidy() + } + await this.config.runHook('update', {channel, version: updated}) CliUx.ux.action.stop() } @@ -249,21 +258,20 @@ export class Updater { CliUx.ux.action.stop() } - private async skipUpdate(current: string, updated: string): Promise { + private notUpdatable(): boolean { if (!this.config.binPath) { const instructions = this.config.scopedEnvVar('UPDATE_INSTRUCTIONS') if (instructions) CliUx.ux.warn(instructions) - return 'not updatable' - } - - if (current === updated) { - if (this.config.scopedEnvVar('HIDE_UPDATED_MESSAGE')) return 'done' - return `already on latest version: ${current}` + return true } return false } + private alreadyOnVersion(current: string, updated: string | null): boolean { + return current === updated + } + private async determineChannel(): Promise { const channelPath = path.join(this.config.dataDir, 'channel') if (fs.existsSync(channelPath)) { From 2d7d4aa16983c596cd3369f2a3f1aeecf5f088a2 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Mon, 7 Feb 2022 09:40:19 -0700 Subject: [PATCH 11/17] chore: update test --- test/update.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/update.test.ts b/test/update.test.ts index 656b2e72..992ccb0e 100644 --- a/test/update.test.ts +++ b/test/update.test.ts @@ -78,7 +78,7 @@ describe('update plugin', () => { updater = initUpdater(config) await updater.runUpdate({autoUpdate: false, hard: false}) const stdout = collector.stdout.join(' ') - expect(stdout).to.include('already on latest version') + expect(stdout).to.include('already on version 2.0.0') }) it('should update to channel', async () => { From be4a5beaa7740d4dfec768657ba622416619e3f8 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 9 Feb 2022 11:24:24 -0700 Subject: [PATCH 12/17] fix: skip preupdate hook if using --hard --- src/commands/update.ts | 4 ++-- src/update.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/commands/update.ts b/src/commands/update.ts index b6984c98..ee617250 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -1,5 +1,5 @@ import {Command, Flags, CliUx} from '@oclif/core' -import {prompt} from 'inquirer' +import {prompt, Separator} from 'inquirer' import * as path from 'path' import {sort} from 'semver' import {Updater} from '../update' @@ -83,7 +83,7 @@ export default class UpdateCommand extends Command { name: 'version', message: 'Select a version to update to', type: 'list', - choices, + choices: [...choices, new Separator()], }) return version } diff --git a/src/update.ts b/src/update.ts index 0a701a27..c3999b2d 100644 --- a/src/update.ts +++ b/src/update.ts @@ -56,7 +56,7 @@ export class Updater { const current = await this.determineCurrentVersion() if (version) { - await this.config.runHook('preupdate', {channel, version}) + if (!hard) await this.config.runHook('preupdate', {channel, version}) const localVersion = await this.findLocalVersion(version) @@ -84,7 +84,7 @@ export class Updater { } else { const manifest = await this.fetchChannelManifest(channel) const updated = manifest.sha ? `${manifest.version}-${manifest.sha}` : manifest.version - await this.config.runHook('preupdate', {channel, version: updated}) + if (!hard) await this.config.runHook('preupdate', {channel, version: updated}) if (!hard && this.alreadyOnVersion(current, updated)) { CliUx.ux.action.stop(this.config.scopedEnvVar('HIDE_UPDATED_MESSAGE') ? 'done' : `already on version ${current}`) From 89525300ad3cfdca964e4198e7f92d7e1d7415be Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 9 Feb 2022 13:30:02 -0700 Subject: [PATCH 13/17] fix: update root after hard update --- src/update.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/update.ts b/src/update.ts index c3999b2d..bd141dde 100644 --- a/src/update.ts +++ b/src/update.ts @@ -47,6 +47,11 @@ export class Updater { return } + // console.log('*'.repeat(process.stdout.columns)) + // const util = require('util') + // console.log(util.inspect(this.config, {depth: 6})) + // console.log('*'.repeat(process.stdout.columns)) + if (hard) { CliUx.ux.action.start(`${this.config.name}: Removing old installations`) await rm(path.dirname(this.clientRoot)) @@ -248,6 +253,7 @@ export class Updater { await this.setChannel(channel) await this.createBin(updated) await this.touch() + this.updateRoot(current, updated) CliUx.ux.action.stop() } @@ -255,6 +261,7 @@ export class Updater { CliUx.ux.action.start(`${this.config.name}: Updating CLI from ${color.green(current)} to ${color.green(updated)}`) await this.createBin(updated) await this.touch() + this.updateRoot(current, updated) CliUx.ux.action.stop() } @@ -377,6 +384,14 @@ export class Updater { } } + private updateRoot(current: string, updated: string): void { + this.config.root = path.join(this.config.root, updated) + const pattern = new RegExp(`${current.split('-')[0]}-.*?(?=\\${path.sep})`) + for (const plugin of this.config.plugins) { + plugin.root = plugin.root.replace(pattern, updated) + } + } + private async createBin(version: string): Promise { const dst = this.clientBin const {bin, windows} = this.config From a5cc8aa4f970a472a6e08b8e72bcbcb4898fd1d4 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Thu, 10 Feb 2022 10:36:59 -0700 Subject: [PATCH 14/17] fix: works with sf --- package.json | 4 ++- src/commands/update.ts | 12 ++++++++- src/update.ts | 57 +++++++++++++++++++++++------------------- test/update.test.ts | 17 +++++++------ 4 files changed, 55 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index aff8f37b..8e69388e 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,9 @@ "commands": "./lib/commands", "bin": "oclif-example", "hooks": { - "init": "./lib/hooks/init" + "init": "./lib/hooks/init", + "preupdate": "./lib/hooks/preupdate", + "update": "./lib/hooks/update" }, "devPlugins": [ "@oclif/plugin-help" diff --git a/src/commands/update.ts b/src/commands/update.ts index ee617250..a9b9a1a9 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -34,18 +34,27 @@ export default class UpdateCommand extends Command { static flags = { autoupdate: Flags.boolean({hidden: true}), - available: Flags.boolean({hidden: true}), + available: Flags.boolean({ + char: 'a', + description: 'Install a specific version.', + }), version: Flags.string({ + char: 'v', description: 'Install a specific version.', exclusive: ['interactive'], }), interactive: Flags.boolean({ + char: 'i', description: 'Interactively select version to install. This is ignored if a channel is provided.', exclusive: ['version'], }), hard: Flags.boolean({ description: 'Remove all existing versions before updating to new version.', }), + 'preserve-links': Flags.boolean({ + hidden: true, + dependsOn: ['hard'], + }), } async run(): Promise { @@ -73,6 +82,7 @@ export default class UpdateCommand extends Command { channel: args.channel, autoUpdate: flags.autoupdate, hard: flags.hard, + preserveLinks: flags['preserve-links'], version: flags.interactive ? await this.promptForVersion(updater) : flags.version, }) } diff --git a/src/update.ts b/src/update.ts index bd141dde..c499b9d6 100644 --- a/src/update.ts +++ b/src/update.ts @@ -22,6 +22,7 @@ export namespace Updater { autoUpdate: boolean; version?: string | undefined hard: boolean; + preserveLinks?: boolean; } export type VersionIndex = Record @@ -37,7 +38,7 @@ export class Updater { } public async runUpdate(options: Updater.Options): Promise { - const {autoUpdate, version, hard} = options + const {autoUpdate, version, hard, preserveLinks = false} = options if (autoUpdate) await this.debounce() CliUx.ux.action.start(`${this.config.name}: Updating CLI`) @@ -47,28 +48,26 @@ export class Updater { return } - // console.log('*'.repeat(process.stdout.columns)) - // const util = require('util') - // console.log(util.inspect(this.config, {depth: 6})) - // console.log('*'.repeat(process.stdout.columns)) - if (hard) { CliUx.ux.action.start(`${this.config.name}: Removing old installations`) - await rm(path.dirname(this.clientRoot)) + await this.hard(preserveLinks) } const channel = options.channel || await this.determineChannel() const current = await this.determineCurrentVersion() if (version) { - if (!hard) await this.config.runHook('preupdate', {channel, version}) - const localVersion = await this.findLocalVersion(version) if (this.alreadyOnVersion(current, localVersion || null)) { CliUx.ux.action.stop(this.config.scopedEnvVar('HIDE_UPDATED_MESSAGE') ? 'done' : `already on version ${current}`) - } else if (localVersion) { - this.updateToExistingVersion(current, localVersion) + return + } + + if (!hard) await this.config.runHook('preupdate', {channel, version}) + + if (localVersion) { + await this.updateToExistingVersion(current, localVersion) } else { const index = await this.fetchVersionIndex() const url = index[version] @@ -79,7 +78,6 @@ export class Updater { const manifest = await this.fetchVersionManifest(version, url) const updated = manifest.sha ? `${manifest.version}-${manifest.sha}` : manifest.version await this.update(manifest, current, updated) - await this.tidy() } await this.config.runHook('update', {channel, version}) @@ -89,19 +87,20 @@ export class Updater { } else { const manifest = await this.fetchChannelManifest(channel) const updated = manifest.sha ? `${manifest.version}-${manifest.sha}` : manifest.version - if (!hard) await this.config.runHook('preupdate', {channel, version: updated}) if (!hard && this.alreadyOnVersion(current, updated)) { CliUx.ux.action.stop(this.config.scopedEnvVar('HIDE_UPDATED_MESSAGE') ? 'done' : `already on version ${current}`) } else { + if (!hard) await this.config.runHook('preupdate', {channel, version: updated}) await this.update(manifest, current, updated, channel) - await this.tidy() } await this.config.runHook('update', {channel, version: updated}) CliUx.ux.action.stop() } + await this.touch() + await this.tidy() CliUx.ux.debug('done') } @@ -128,6 +127,19 @@ export class Updater { } } + private async hard(preserveLinks: boolean): Promise { + if (preserveLinks) { + const files = await ls(path.dirname(this.clientRoot)) + const filtered = files.filter(f => !f.path.includes('package.json')) + for (const file of filtered) { + // eslint-disable-next-line no-await-in-loop + await rm(file.path) + } + } else { + await rm(path.dirname(this.clientRoot)) + } + } + private async ensureClientDir(): Promise { try { await fs.mkdirp(this.clientRoot) @@ -250,19 +262,16 @@ export class Updater { await this.downloadAndExtract(output, manifest, channel) } + await this.refreshConfig(updated) await this.setChannel(channel) await this.createBin(updated) - await this.touch() - this.updateRoot(current, updated) - CliUx.ux.action.stop() } private async updateToExistingVersion(current: string, updated: string): Promise { CliUx.ux.action.start(`${this.config.name}: Updating CLI from ${color.green(current)} to ${color.green(updated)}`) + await this.ensureClientDir() + await this.refreshConfig(updated) await this.createBin(updated) - await this.touch() - this.updateRoot(current, updated) - CliUx.ux.action.stop() } private notUpdatable(): boolean { @@ -384,12 +393,8 @@ export class Updater { } } - private updateRoot(current: string, updated: string): void { - this.config.root = path.join(this.config.root, updated) - const pattern = new RegExp(`${current.split('-')[0]}-.*?(?=\\${path.sep})`) - for (const plugin of this.config.plugins) { - plugin.root = plugin.root.replace(pattern, updated) - } + private async refreshConfig(version: string): Promise { + this.config = await Config.load({root: path.join(this.clientRoot, version)}) as Config } private async createBin(version: string): Promise { diff --git a/test/update.test.ts b/test/update.test.ts index 992ccb0e..1d315b3d 100644 --- a/test/update.test.ts +++ b/test/update.test.ts @@ -54,6 +54,9 @@ describe('update plugin', () => { sandbox.stub(CliUx.ux, 'warn').callsFake(line => collector.stderr.push(line ? `${line}` : '')) sandbox.stub(CliUx.ux.action, 'start').callsFake(line => collector.stdout.push(line || '')) sandbox.stub(CliUx.ux.action, 'stop').callsFake(line => collector.stdout.push(line || '')) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + sandbox.stub(Updater.prototype, 'refreshConfig').resolves() }) afterEach(() => { @@ -170,12 +173,12 @@ describe('update plugin', () => { clientRoot = setupClientRoot({config}) const platformRegex = new RegExp(`tarballs\\/example-cli\\/${config.platform}-${config.arch}`) const manifestRegex = new RegExp(`channels\\/stable\\/example-cli-${config.platform}-${config.arch}-buildmanifest`) - const tarballRegex = new RegExp(`tarballs\\/example-cli\\/example-cli-v2.0.0\\/example-cli-v2.0.0-${config.platform}-${config.arch}gz`) - const newVersionPath = path.join(clientRoot, '2.0.0') + const tarballRegex = new RegExp(`tarballs\\/example-cli\\/example-cli-v2.0.0\\/example-cli-v2.0.1-${config.platform}-${config.arch}gz`) + const newVersionPath = path.join(clientRoot, '2.0.1') fs.mkdirpSync(path.join(newVersionPath, 'bin')) fs.mkdirpSync(path.join(`${newVersionPath}.partial.11111`, 'bin')) - fs.writeFileSync(path.join(`${newVersionPath}.partial.11111`, 'bin', 'example-cli'), '../2.0.0/bin', 'utf8') - fs.writeFileSync(path.join(newVersionPath, 'bin', 'example-cli'), '../2.0.0/bin', 'utf8') + fs.writeFileSync(path.join(`${newVersionPath}.partial.11111`, 'bin', 'example-cli'), '../2.0.1/bin', 'utf8') + fs.writeFileSync(path.join(newVersionPath, 'bin', 'example-cli'), '../2.0.1/bin', 'utf8') sandbox.stub(extract, 'extract').resolves() sandbox.stub(zlib, 'gzipSync').returns(Buffer.alloc(1, ' ')) @@ -183,9 +186,9 @@ describe('update plugin', () => { nock(/oclif-staging.s3.amazonaws.com/) .get(platformRegex) - .reply(200, {version: '2.0.0'}) + .reply(200, {version: '2.0.1'}) .get(manifestRegex) - .reply(200, {version: '2.0.0'}) + .reply(200, {version: '2.0.1'}) .get(tarballRegex) .reply(200, gzContents, { 'X-Transfer-Length': String(gzContents.length), @@ -194,7 +197,7 @@ describe('update plugin', () => { }) updater = initUpdater(config) - await updater.runUpdate({autoUpdate: false, hard: false, version: '2.0.0'}) + await updater.runUpdate({autoUpdate: false, hard: false, version: '2.0.1'}) const stdout = stripAnsi(collector.stdout.join(' ')) expect(stdout).to.matches(/Updating to a specific version will not update the channel/) }) From 2bd09dc919a658d4f08a8cfc97d6c4df27fddca8 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Thu, 10 Feb 2022 14:22:22 -0700 Subject: [PATCH 15/17] fix: replace --hard with --force --- package.json | 4 +--- src/commands/update.ts | 11 +++------- src/update.ts | 46 ++++++++++++------------------------------ src/util.ts | 5 ++--- test/update.test.ts | 10 ++++----- 5 files changed, 24 insertions(+), 52 deletions(-) diff --git a/package.json b/package.json index 8e69388e..aff8f37b 100644 --- a/package.json +++ b/package.json @@ -63,9 +63,7 @@ "commands": "./lib/commands", "bin": "oclif-example", "hooks": { - "init": "./lib/hooks/init", - "preupdate": "./lib/hooks/preupdate", - "update": "./lib/hooks/update" + "init": "./lib/hooks/init" }, "devPlugins": [ "@oclif/plugin-help" diff --git a/src/commands/update.ts b/src/commands/update.ts index a9b9a1a9..541f4294 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -48,12 +48,8 @@ export default class UpdateCommand extends Command { description: 'Interactively select version to install. This is ignored if a channel is provided.', exclusive: ['version'], }), - hard: Flags.boolean({ - description: 'Remove all existing versions before updating to new version.', - }), - 'preserve-links': Flags.boolean({ - hidden: true, - dependsOn: ['hard'], + force: Flags.boolean({ + description: 'Force a re-download of the requested version.', }), } @@ -81,8 +77,7 @@ export default class UpdateCommand extends Command { return updater.runUpdate({ channel: args.channel, autoUpdate: flags.autoupdate, - hard: flags.hard, - preserveLinks: flags['preserve-links'], + force: flags.force, version: flags.interactive ? await this.promptForVersion(updater) : flags.version, }) } diff --git a/src/update.ts b/src/update.ts index c499b9d6..b0deb8a3 100644 --- a/src/update.ts +++ b/src/update.ts @@ -9,7 +9,7 @@ import throttle from 'lodash.throttle' import fileSize from 'filesize' import {extract} from './tar' -import {ls, rm, wait} from './util' +import {ls, wait} from './util' const filesize = (n: number): string => { const [num, suffix] = fileSize(n, {output: 'array'}) @@ -18,11 +18,10 @@ const filesize = (n: number): string => { export namespace Updater { export type Options = { - channel?: string | undefined; autoUpdate: boolean; + channel?: string | undefined; version?: string | undefined - hard: boolean; - preserveLinks?: boolean; + force?: boolean; } export type VersionIndex = Record @@ -38,7 +37,7 @@ export class Updater { } public async runUpdate(options: Updater.Options): Promise { - const {autoUpdate, version, hard, preserveLinks = false} = options + const {autoUpdate, version, force = false} = options if (autoUpdate) await this.debounce() CliUx.ux.action.start(`${this.config.name}: Updating CLI`) @@ -48,23 +47,18 @@ export class Updater { return } - if (hard) { - CliUx.ux.action.start(`${this.config.name}: Removing old installations`) - await this.hard(preserveLinks) - } - const channel = options.channel || await this.determineChannel() const current = await this.determineCurrentVersion() if (version) { - const localVersion = await this.findLocalVersion(version) + const localVersion = force ? null : await this.findLocalVersion(version) if (this.alreadyOnVersion(current, localVersion || null)) { CliUx.ux.action.stop(this.config.scopedEnvVar('HIDE_UPDATED_MESSAGE') ? 'done' : `already on version ${current}`) return } - if (!hard) await this.config.runHook('preupdate', {channel, version}) + await this.config.runHook('preupdate', {channel, version}) if (localVersion) { await this.updateToExistingVersion(current, localVersion) @@ -77,7 +71,7 @@ export class Updater { const manifest = await this.fetchVersionManifest(version, url) const updated = manifest.sha ? `${manifest.version}-${manifest.sha}` : manifest.version - await this.update(manifest, current, updated) + await this.update(manifest, current, updated, force, channel) } await this.config.runHook('update', {channel, version}) @@ -88,11 +82,11 @@ export class Updater { const manifest = await this.fetchChannelManifest(channel) const updated = manifest.sha ? `${manifest.version}-${manifest.sha}` : manifest.version - if (!hard && this.alreadyOnVersion(current, updated)) { + if (!force && this.alreadyOnVersion(current, updated)) { CliUx.ux.action.stop(this.config.scopedEnvVar('HIDE_UPDATED_MESSAGE') ? 'done' : `already on version ${current}`) } else { - if (!hard) await this.config.runHook('preupdate', {channel, version: updated}) - await this.update(manifest, current, updated, channel) + await this.config.runHook('preupdate', {channel, version: updated}) + await this.update(manifest, current, updated, force, channel) } await this.config.runHook('update', {channel, version: updated}) @@ -127,19 +121,6 @@ export class Updater { } } - private async hard(preserveLinks: boolean): Promise { - if (preserveLinks) { - const files = await ls(path.dirname(this.clientRoot)) - const filtered = files.filter(f => !f.path.includes('package.json')) - for (const file of filtered) { - // eslint-disable-next-line no-await-in-loop - await rm(file.path) - } - } else { - await rm(path.dirname(this.clientRoot)) - } - } - private async ensureClientDir(): Promise { try { await fs.mkdirp(this.clientRoot) @@ -252,15 +233,14 @@ export class Updater { await extraction } - private async update(manifest: Interfaces.S3Manifest, current: string, updated: string, channel = 'stable') { + // eslint-disable-next-line max-params + private async update(manifest: Interfaces.S3Manifest, current: string, updated: string, force: boolean, channel: string) { CliUx.ux.action.start(`${this.config.name}: Updating CLI from ${color.green(current)} to ${color.green(updated)}${channel === 'stable' ? '' : ' (' + color.yellow(channel) + ')'}`) await this.ensureClientDir() const output = path.join(this.clientRoot, updated) - if (!await fs.pathExists(output)) { - await this.downloadAndExtract(output, manifest, channel) - } + if (force || !await fs.pathExists(output)) await this.downloadAndExtract(output, manifest, channel) await this.refreshConfig(updated) await this.setChannel(channel) diff --git a/src/util.ts b/src/util.ts index 99cc5513..52746158 100644 --- a/src/util.ts +++ b/src/util.ts @@ -16,9 +16,8 @@ export async function ls(dir: string): Promise { - return new Promise((resolve, reject) => { - fs.rm(dir, {recursive: true}, (err: Error | null) => { - if (err) reject(err) + return new Promise(resolve => { + fs.rm(dir, {recursive: true, force: true}, () => { resolve() }) }) diff --git a/test/update.test.ts b/test/update.test.ts index 1d315b3d..0f0ff00e 100644 --- a/test/update.test.ts +++ b/test/update.test.ts @@ -79,7 +79,7 @@ describe('update plugin', () => { .reply(200, {version: '2.0.0'}) updater = initUpdater(config) - await updater.runUpdate({autoUpdate: false, hard: false}) + await updater.runUpdate({autoUpdate: false}) const stdout = collector.stdout.join(' ') expect(stdout).to.include('already on version 2.0.0') }) @@ -110,7 +110,7 @@ describe('update plugin', () => { }) updater = initUpdater(config) - await updater.runUpdate({autoUpdate: false, hard: false}) + await updater.runUpdate({autoUpdate: false}) const stdout = stripAnsi(collector.stdout.join(' ')) expect(stdout).to.matches(/Updating CLI from 2.0.0 to 2.0.1/) }) @@ -148,7 +148,7 @@ describe('update plugin', () => { }) updater = initUpdater(config) - await updater.runUpdate({autoUpdate: false, hard: false, version: '2.0.1'}) + await updater.runUpdate({autoUpdate: false, version: '2.0.1'}) const stdout = stripAnsi(collector.stdout.join(' ')) expect(stdout).to.matches(/Updating CLI from 2.0.0 to 2.0.1/) }) @@ -164,7 +164,7 @@ describe('update plugin', () => { .reply(200, {version: '2.0.0'}) updater = initUpdater(config) - await updater.runUpdate({autoUpdate: false, hard: false}) + await updater.runUpdate({autoUpdate: false}) const stdout = collector.stdout.join(' ') expect(stdout).to.include('not updatable') }) @@ -197,7 +197,7 @@ describe('update plugin', () => { }) updater = initUpdater(config) - await updater.runUpdate({autoUpdate: false, hard: false, version: '2.0.1'}) + await updater.runUpdate({autoUpdate: false, version: '2.0.1'}) const stdout = stripAnsi(collector.stdout.join(' ')) expect(stdout).to.matches(/Updating to a specific version will not update the channel/) }) From e30760426b7bd260925901b16b94ece363287af5 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Thu, 10 Feb 2022 17:22:20 -0700 Subject: [PATCH 16/17] chore: update examples --- src/commands/update.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/commands/update.ts b/src/commands/update.ts index 541f4294..0840f55e 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -23,12 +23,8 @@ export default class UpdateCommand extends Command { command: '<%= config.bin %> <%= command.id %> --interactive', }, { - description: 'Remove all existing versions and install stable channel version:', - command: '<%= config.bin %> <%= command.id %> stable --hard', - }, - { - description: 'Remove all existing versions and install specific version:', - command: '<%= config.bin %> <%= command.id %> --version 1.0.0 --hard', + description: 'See available versions:', + command: '<%= config.bin %> <%= command.id %> --available', }, ] From 34777d12fb5cbdb6b6f9084b8e916eeaa2ff31ee Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Fri, 11 Feb 2022 14:43:16 -0700 Subject: [PATCH 17/17] chore: bump to 3.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index aff8f37b..851105ae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@oclif/plugin-update", - "version": "2.2.0", + "version": "3.0.0", "author": "Salesforce", "bugs": "https://github.com/oclif/plugin-update/issues", "dependencies": {