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 5a13f3c2..851105ae 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,18 @@ { "name": "@oclif/plugin-update", - "version": "2.2.0", + "version": "3.0.0", "author": "Salesforce", "bugs": "https://github.com/oclif/plugin-update/issues", "dependencies": { "@oclif/color": "^1.0.0", - "@oclif/core": "^1.2.0", - "@types/semver": "^7.3.4", + "@oclif/core": "^1.3.0", "cross-spawn": "^7.0.3", "debug": "^4.3.1", "filesize": "^6.1.0", "fs-extra": "^9.0.1", "http-call": "^5.3.0", - "lodash": "^4.17.21", + "inquirer": "^8.2.0", + "lodash.throttle": "^4.1.1", "log-chopper": "^1.0.2", "semver": "^7.3.5", "tar-fs": "^2.1.1" @@ -25,9 +25,11 @@ "@types/execa": "^0.9.0", "@types/fs-extra": "^8.0.1", "@types/glob": "^7.1.3", - "@types/lodash": "^4.14.168", + "@types/inquirer": "^8.2.0", + "@types/lodash.throttle": "^4.1.6", "@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 +39,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 93e708b3..0840f55e 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -1,30 +1,91 @@ -import {Command, Flags, Config, CliUx} from '@oclif/core' +import {Command, Flags, CliUx} from '@oclif/core' +import {prompt, Separator} from 'inquirer' import * as path from 'path' -import UpdateCli from '../update' - -async function getPinToVersion(): Promise { - return CliUx.ux.prompt('Enter a version to update to') -} +import {sort} from 'semver' +import {Updater} from '../update' export default class UpdateCommand extends Command { static description = 'update the <%= config.bin %> CLI' 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: 'Interactively select version:', + command: '<%= config.bin %> <%= command.id %> --interactive', + }, + { + description: 'See available versions:', + command: '<%= config.bin %> <%= command.id %> --available', + }, + ] + static flags = { autoupdate: Flags.boolean({hidden: true}), - 'from-local': Flags.boolean({description: 'interactively choose an already installed version'}), + 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'], + }), + force: Flags.boolean({ + description: 'Force a re-download of the requested version.', + }), } - private channel!: string + async run(): Promise { + const {args, flags} = await this.parse(UpdateCommand) + const updater = new Updater(this.config) + if (flags.available) { + const index = await updater.fetchVersionIndex() + const allVersions = sort(Object.keys(index)).reverse() + const localVersions = await updater.findLocalVersions() + + const table = allVersions.map(version => { + const location = localVersions.find(l => path.basename(l).startsWith(version)) || index[version] + return {version, location} + }) - private readonly clientRoot = this.config.scopedEnvVar('OCLIF_CLIENT_HOME') || path.join(this.config.dataDir, 'client') + CliUx.ux.table(table, {version: {}, location: {}}) + return + } - private readonly clientBin = path.join(this.clientRoot, 'bin', this.config.windows ? `${this.config.bin}.cmd` : this.config.bin) + if (args.channel && flags.version) { + this.error('You cannot specifiy both a version and a channel.') + } - 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}) - return updateCli.runUpdate() + return updater.runUpdate({ + channel: args.channel, + autoUpdate: flags.autoupdate, + force: flags.force, + version: flags.interactive ? await this.promptForVersion(updater) : flags.version, + }) + } + + 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', + type: 'list', + choices: [...choices, new Separator()], + }) + return version } } 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/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 558a8f92..b0deb8a3 100644 --- a/src/update.ts +++ b/src/update.ts @@ -1,149 +1,222 @@ /* 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 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, wait} from './util' -export interface UpdateCliOptions { - channel?: string; - autoUpdate: boolean; - fromLocal: boolean; - config: Config; - exit: any; - getPinToVersion: () => Promise; +const filesize = (n: number): string => { + const [num, suffix] = fileSize(n, {output: 'array'}) + return Number.parseFloat(num).toFixed(1) + ` ${suffix}` } -export default class UpdateCli { - private channel!: string - - private currentVersion?: string +export namespace Updater { + export type Options = { + autoUpdate: boolean; + channel?: string | undefined; + version?: string | undefined + force?: boolean; + } - private updatedVersion!: string + export type VersionIndex = Record +} +export class Updater { private readonly clientRoot: string - private readonly clientBin: string - 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) + 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) } - async runUpdate(): Promise { - if (this.options.autoUpdate) await this.debounce() - - this.channel = this.options.channel || await this.determineChannel() + public async runUpdate(options: Updater.Options): Promise { + const {autoUpdate, version, force = false} = options + if (autoUpdate) await this.debounce() - if (this.options.fromLocal) { - await this.ensureClientDir() - CliUx.ux.debug(`Looking for locally installed versions at ${this.clientRoot}`) + CliUx.ux.action.start(`${this.config.name}: Updating CLI`) - // 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.') + if (this.notUpdatable()) { + CliUx.ux.action.stop('not updatable') + return + } - CliUx.ux.log(`Found versions: \n${versions.map(version => ` ${version}`).join('\n')}\n`) + const channel = options.channel || await this.determineChannel() + const current = await this.determineCurrentVersion() - const pinToVersion = await this.options.getPinToVersion() - if (!versions.includes(pinToVersion)) throw new Error(`Version ${pinToVersion} not found in the locally installed versions.`) + if (version) { + const localVersion = force ? null : await this.findLocalVersion(version) - if (!await fs.pathExists(path.join(this.clientRoot, pinToVersion))) { - throw new Error(`Version ${pinToVersion} is not already installed at ${this.clientRoot}.`) + if (this.alreadyOnVersion(current, localVersion || null)) { + CliUx.ux.action.stop(this.config.scopedEnvVar('HIDE_UPDATED_MESSAGE') ? 'done' : `already on version ${current}`) + return } - CliUx.ux.action.start(`${this.options.config.name}: Updating CLI`) - CliUx.ux.debug(`switching to existing version ${pinToVersion}`) - this.updateToExistingVersion(pinToVersion) + await this.config.runHook('preupdate', {channel, version}) + + if (localVersion) { + await 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 + await this.update(manifest, current, updated, force, channel) + } + await this.config.runHook('update', {channel, version}) + CliUx.ux.action.stop() 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}.`) + 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 { - CliUx.ux.action.start(`${this.options.config.name}: Updating CLI`) - await this.options.config.runHook('preupdate', {channel: this.channel}) - const manifest = await this.fetchManifest() - this.currentVersion = await this.determineCurrentVersion() - this.updatedVersion = (manifest as any).sha ? `${manifest.version}-${(manifest as any).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}) + const manifest = await this.fetchChannelManifest(channel) + const updated = manifest.sha ? `${manifest.version}-${manifest.sha}` : manifest.version + + if (!force && this.alreadyOnVersion(current, updated)) { + CliUx.ux.action.stop(this.config.scopedEnvVar('HIDE_UPDATED_MESSAGE') ? 'done' : `already on version ${current}`) + } else { + await this.config.runHook('preupdate', {channel, version: updated}) + await this.update(manifest, current, updated, force, channel) + } + + await this.config.runHook('update', {channel, version: updated}) + CliUx.ux.action.stop() } + await this.touch() + await this.tidy() CliUx.ux.debug('done') - CliUx.ux.action.stop() } - private async fetchManifest(): Promise { - const http: typeof HTTP = require('http-call').HTTP - - CliUx.ux.action.status = 'fetching manifest' + public async findLocalVersions(): Promise { + await this.ensureClientDir() + return fs + .readdirSync(this.clientRoot) + .filter(dirOrFile => dirOrFile !== 'bin' && dirOrFile !== 'current') + .map(f => path.join(this.clientRoot, f)) + } + public async fetchVersionIndex(): Promise { + CliUx.ux.action.status = 'fetching version index' + const newIndexUrl = this.config.s3Url(this.s3VersionIndexKey()) 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 {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` + 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}.`) + } + } + + 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 + } + } + } + + 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 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 s3VersionIndexKey(): string { + const {bin, platform, arch} = this.config + const s3SubDir = this.composeS3SubDir() + return path.join(s3SubDir, 'versions', `${bin}-${platform}-${arch}-tar-gz.json`) + } + + 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 ${this.channel}`) + if (error.statusCode === 403) throw new Error(`HTTP 403: Invalid channel ${channel}`) throw error } } - private async downloadAndExtract(output: string, manifest: IManifest, channel: string) { - const {version, gz, sha256gz} = manifest + 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(version, hash) + return this.fetchManifest(s3Key) + } - const filesize = (n: number): string => { - const [num, suffix] = require('filesize')(n, {output: 'array'}) - return num.toFixed(1) + ` ${suffix}` + private async fetchManifest(s3Key: string): Promise { + CliUx.ux.action.status = 'fetching manifest' + + const url = this.config.s3Url(s3Key) + const {body} = await HTTP.get(url) + if (typeof body === 'string') { + return JSON.parse(body) } - const http: typeof HTTP = require('http-call').HTTP - const gzUrl = gz || this.options.config.s3Url(this.options.config.s3Key('versioned', { + return body + } + + private async downloadAndExtract(output: string, manifest: Interfaces.S3Manifest, channel: string) { + const {version, gz, sha256gz} = manifest + + 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 }, @@ -160,86 +233,86 @@ export default class UpdateCli { await extraction } - private async update(manifest: IManifest, 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) + ')'}`) + // 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, this.updatedVersion) + 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.setChannel() - await this.createBin(this.updatedVersion) - await this.touch() - await this.reexec() + await this.refreshConfig(updated) + await this.setChannel(channel) + await this.createBin(updated) } - private async updateToExistingVersion(version: string) { - await this.createBin(version) - await this.touch() + 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) } - private async skipUpdate(): Promise { - if (!this.options.config.binPath) { - const instructions = this.options.config.scopedEnvVar('UPDATE_INSTRUCTIONS') + private notUpdatable(): boolean { + 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}` + return true } return false } + private alreadyOnVersion(current: string, updated: string | null): boolean { + return current === updated + } + 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 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`) + private async findLocalVersion(version: string): Promise { + const versions = await this.findLocalVersions() + return versions + .map(file => path.basename(file)) + .find(file => file.startsWith(version)) } - private async setChannel() { - 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() { + 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) } } - private async mtime(f: string) { + private async mtime(f: string): Promise { const {mtime} = await fs.stat(f) return mtime } @@ -247,7 +320,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()) { @@ -255,7 +328,7 @@ export default class UpdateCli { if (output) { CliUx.ux.debug(msg) } else { - await CliUx.ux.log(msg) + CliUx.ux.log(msg) output = true } @@ -267,13 +340,14 @@ export default class UpdateCli { } // removes any unused CLIs - private async tidy() { + 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()) { @@ -287,10 +361,10 @@ 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) + 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()) @@ -299,30 +373,15 @@ 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 refreshConfig(version: string): Promise { + this.config = await Config.load({root: path.join(this.clientRoot, version)}) as Config } - 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') - 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 @@ -359,19 +418,4 @@ ${binPathEnvVar}="\$DIR/${bin}" ${redirectedEnvVar}=1 "$DIR/../${version}/bin/${ await fs.symlink(`./${version}`, path.join(this.clientRoot, 'current')) } } - - private async ensureClientDir() { - 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/src/util.ts b/src/util.ts index 070617cd..52746158 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,12 +9,20 @@ 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})))) } +export async function rm(dir: string): Promise { + return new Promise(resolve => { + fs.rm(dir, {recursive: true, force: true}, () => { + resolve() + }) + }) +} + 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 94080fce..0f0ff00e 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,27 +32,21 @@ function setupClientRoot(ctx: { config: IConfig }, createVersion?: string): stri return clientRoot } -function initUpdateCli(options: Partial): UpdateCli { - const updateCli = new UpdateCli({channel: options.channel, - fromLocal: options.fromLocal || false, - autoUpdate: options.autoUpdate || false, - config: options.config!, - exit: undefined, - getPinToVersion: async () => '2.0.0', - }) - 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() @@ -60,7 +54,11 @@ 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(() => { nock.cleanAll() if (fs.pathExistsSync(clientRoot)) { @@ -69,6 +67,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}`) @@ -79,24 +78,20 @@ 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() + updater = initUpdater(config) + await updater.runUpdate({autoUpdate: 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', 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`) 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(UpdateCli.prototype, 'reexec' as any).resolves() sandbox.stub(extract, 'extract').resolves() sandbox.stub(zlib, 'gzipSync').returns(Buffer.alloc(1, ' ')) @@ -114,11 +109,50 @@ describe('update plugin', () => { 'Content-Encoding': 'gzip', }) - updateCli = initUpdateCli({config: config as Config}) - await updateCli.runUpdate() + updater = initUpdater(config) + 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/) + }) + + 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`) + + 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`, + }) + + updater = initUpdater(config) + 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/) }) + it('should not update - not updatable', async () => { clientRoot = setupClientRoot({config}) // unset binPath @@ -129,24 +163,22 @@ 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() + updater = initUpdater(config) + await updater.runUpdate({autoUpdate: 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 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') - sandbox.stub(UpdateCli.prototype, 'reexec' as any).resolves() + 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, ' ')) @@ -154,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), @@ -164,9 +196,9 @@ describe('update plugin', () => { 'Content-Encoding': 'gzip', }) - updateCli = initUpdateCli({fromLocal: true, config: config as Config, getPinToVersion: async () => '2.0.0'}) - await updateCli.runUpdate() + updater = initUpdater(config) + await updater.runUpdate({autoUpdate: false, version: '2.0.1'}) 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/) }) }) 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 a00c9ec4..8f04a681 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== @@ -724,7 +700,7 @@ "@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" + resolved "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.2.tgz#168309de311cd30a2b8ae720de6475c2fbf33ac7" integrity sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw== dependencies: "@types/node" "*" @@ -756,12 +732,27 @@ "@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" 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== @@ -818,6 +809,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 +1146,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 +1252,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 +1528,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 +1910,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 +2239,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 +2703,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 +2927,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 +3007,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 +3188,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 +3213,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" @@ -3450,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" @@ -3632,13 +3599,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 +4020,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 +4405,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 +4451,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 +4480,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 +4742,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 +5411,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 +5602,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"