diff --git a/src/commands/install.ts b/src/commands/install.ts index ddac9e0b..4f222728 100644 --- a/src/commands/install.ts +++ b/src/commands/install.ts @@ -1,72 +1,75 @@ -import cli from 'cli-ux' -import * as semver from 'semver' -import * as fs from 'fs-extra' +import cli from 'cli-ux'; +import * as semver from 'semver'; +import * as fs from 'fs-extra'; -import UpdateCommand from './update' +import UpdateCommand from './update'; -const SEMVER_REGEX = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+)?/ +const SEMVER_REGEX = + /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+)?/; export default class InstallCommand extends UpdateCommand { - static args = [{name: 'version', optional: false}] + static args = [{ name: 'version', optional: false }]; - static flags = {} + static flags = {}; - async run() { - const {args} = this.parse(InstallCommand) + async run() { + const { args } = this.parse(InstallCommand); - // Check if this command is trying to update the channel. TODO: make this dynamic - const prereleaseChannels = ['alpha', 'beta', 'next'] - const isExplicitVersion = SEMVER_REGEX.test(args.version || '') - const channelUpdateRequested = ['stable', ...prereleaseChannels].some( - c => args.version === c, - ) + // Check if this command is trying to update the channel. TODO: make this dynamic + const prereleaseChannels = ['alpha', 'beta', 'next']; + const isExplicitVersion = SEMVER_REGEX.test(args.version || ''); + const channelUpdateRequested = ['stable', ...prereleaseChannels].some( + (c) => args.version === c, + ); - if (!isExplicitVersion && !channelUpdateRequested) { - throw new Error( - `Invalid argument provided: ${args.version}. Please specify either a valid channel (alpha, beta, next, stable) or an explicit version (ex. 2.68.13)`, - ) - } + if (!isExplicitVersion && !channelUpdateRequested) { + throw new Error( + `Invalid argument provided: ${args.version}. Please specify either a valid channel (alpha, beta, next, stable) or an explicit version (ex. 2.68.13)`, + ); + } - this.channel = channelUpdateRequested ? - args.version : - await this.determineChannel() + this.channel = channelUpdateRequested + ? args.version + : await this.determineChannel(); - const targetVersion = semver.clean(args.version) || args.version - // Determine if the version is from a different channel and update to account for it (ex. cli-example update 3.0.0-next.22 should update the channel to next as well.) - const versionParts = targetVersion?.split('-') || ['', ''] - if (versionParts && versionParts[1]) { - this.channel = versionParts[1].substr(0, versionParts[1].indexOf('.')) - this.debug(`Flag overriden target channel: ${this.channel}`) - } + const targetVersion = semver.clean(args.version) || args.version; + // Determine if the version is from a different channel and update to account for it (ex. cli-example update 3.0.0-next.22 should update the channel to next as well.) + const versionParts = targetVersion?.split('-') || ['', '']; + if (versionParts && versionParts[1]) { + this.channel = versionParts[1].substr(0, versionParts[1].indexOf('.')); + this.debug(`Flag overriden target channel: ${this.channel}`); + } - const versions = fs + const versions = fs .readdirSync(this.clientRoot) - .filter(dirOrFile => dirOrFile !== 'bin' && dirOrFile !== 'current') + .filter((dirOrFile) => dirOrFile !== 'bin' && dirOrFile !== 'current'); - if (versions.includes(targetVersion)) { - await this.updateToExistingVersion(targetVersion) - this.currentVersion = await this.determineCurrentVersion() - this.updatedVersion = targetVersion - if (channelUpdateRequested) { - await this.setChannel() - } - } else { - const explicitVersion = isExplicitVersion ? targetVersion : null - cli.action.start(`${this.config.name}: Updating CLI`) - await this.config.runHook('preupdate', {channel: this.channel}) - const manifest = await this.fetchManifest() - this.currentVersion = await this.determineCurrentVersion() + if (versions.includes(targetVersion)) { + await this.updateToExistingVersion(targetVersion); + this.currentVersion = await this.determineCurrentVersion(); + this.updatedVersion = targetVersion; + if (channelUpdateRequested) { + await this.setChannel(); + } + } else { + const explicitVersion = isExplicitVersion ? targetVersion : null; + cli.action.start(`${this.config.name}: Updating CLI`); + await this.config.runHook('preupdate', { channel: this.channel }); + const manifest = await this.fetchManifest(); + this.currentVersion = await this.determineCurrentVersion(); - this.updatedVersion = (manifest as any).sha ? `${targetVersion}-${(manifest as any).sha}` : targetVersion - this.debug(`Updating to ${this.updatedVersion}`) - const reason = await this.skipUpdate() - if (reason) cli.action.stop(reason || 'done') - else await this.update(manifest, this.channel, explicitVersion) - this.debug('tidy') - await this.tidy() - await this.config.runHook('update', {channel: this.channel}) + this.updatedVersion = (manifest as any).sha + ? `${targetVersion}-${(manifest as any).sha}` + : targetVersion; + this.debug(`Updating to ${this.updatedVersion}`); + const reason = await this.skipUpdate(); + if (reason) cli.action.stop(reason || 'done'); + else await this.update(manifest, this.channel, explicitVersion); + this.debug('tidy'); + await this.tidy(); + await this.config.runHook('update', { channel: this.channel }); - this.debug('done') - cli.action.stop() - } + this.debug('done'); + cli.action.stop(); } + } } diff --git a/src/commands/update.ts b/src/commands/update.ts index a9f0c9d6..7967b253 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -154,34 +154,46 @@ export default class UpdateCommand extends Command { } } - protected async downloadAndExtract(output: string, manifest: IManifest, channel: string, targetVersion?: string) { - const {version} = manifest + protected async downloadAndExtract( + output: string, + manifest: IManifest, + channel: string, + targetVersion?: string, + ) { + const { version } = manifest; const filesize = (n: number): string => { - const [num, suffix] = require('filesize')(n, {output: 'array'}) - return num.toFixed(1) + ` ${suffix}` - } + const [num, suffix] = require('filesize')(n, { output: 'array' }); + return num.toFixed(1) + ` ${suffix}`; + }; - const http: typeof HTTP = require('http-call').HTTP - const gzUrl = !targetVersion && manifest.gz ? manifest.gz : this.config.s3Url(this.config.s3Key('versioned', { - version: targetVersion, - channel, - bin: this.config.bin, - platform: this.config.platform, - arch: this.config.arch, - ext: targetVersion ? 'tar.gz' : 'gz', - })) - const {response: stream} = await http.stream(gzUrl) - stream.pause() - - const baseDir = manifest.baseDir || this.config.s3Key('baseDir', { - version, - channel, - bin: this.config.bin, - platform: this.config.platform, - arch: this.config.arch, - }) - const extraction = extract(stream, baseDir, output, manifest.sha256gz) + const http: typeof HTTP = require('http-call').HTTP; + const gzUrl = + !targetVersion && manifest.gz + ? manifest.gz + : this.config.s3Url( + this.config.s3Key('versioned', { + version: targetVersion, + channel, + bin: this.config.bin, + platform: this.config.platform, + arch: this.config.arch, + ext: targetVersion ? 'tar.gz' : 'gz', + }), + ); + const { response: stream } = await http.stream(gzUrl); + stream.pause(); + + const baseDir = + manifest.baseDir || + this.config.s3Key('baseDir', { + version, + channel, + bin: this.config.bin, + platform: this.config.platform, + arch: this.config.arch, + }); + const extraction = extract(stream, baseDir, output, manifest.sha256gz); // to-do: use cli.action.type if ((cli.action as any).frames) { @@ -205,16 +217,26 @@ export default class UpdateCommand extends Command { await extraction; } - protected async update(manifest: IManifest, channel = this.channel, targetVersion?: string) { - const {channel: manifestChannel} = manifest - if (manifestChannel) channel = manifestChannel - cli.action.start(`${this.config.name}: Updating CLI from ${color.green(this.currentVersion)} to ${color.green(this.updatedVersion)}${channel === 'stable' ? '' : ' (' + color.yellow(channel) + ')'}`) + protected async update( + manifest: IManifest, + channel = this.channel, + targetVersion?: string, + ) { + const { channel: manifestChannel } = manifest; + if (manifestChannel) channel = manifestChannel; + cli.action.start( + `${this.config.name}: Updating CLI from ${color.green( + this.currentVersion, + )} to ${color.green(this.updatedVersion)}${ + channel === 'stable' ? '' : ' (' + color.yellow(channel) + ')' + }`, + ); - await this.ensureClientDir() - const output = path.join(this.clientRoot, this.updatedVersion) + await this.ensureClientDir(); + const output = path.join(this.clientRoot, this.updatedVersion); - if (!await fs.pathExists(output)) { - await this.downloadAndExtract(output, manifest, channel, targetVersion) + if (!(await fs.pathExists(output))) { + await this.downloadAndExtract(output, manifest, channel, targetVersion); } await this.setChannel(); diff --git a/test/commands/install.test.ts b/test/commands/install.test.ts index 61a3940b..46fc7e98 100644 --- a/test/commands/install.test.ts +++ b/test/commands/install.test.ts @@ -1,10 +1,10 @@ -import InstallCommand from "../../src/commands/install"; -import * as fs from "fs-extra"; -import { mocked } from "ts-jest/utils"; -import { IConfig } from "@oclif/config"; +import InstallCommand from '../../src/commands/install'; +import * as fs from 'fs-extra'; +import { mocked } from 'ts-jest/utils'; +import { IConfig } from '@oclif/config'; -jest.mock("fs-extra"); -jest.mock("http-call", () => ({ +jest.mock('fs-extra'); +jest.mock('http-call', () => ({ HTTP: { get: jest.fn(), }, @@ -31,32 +31,32 @@ class MockedInstallCommand extends InstallCommand { public updateToExistingVersion = jest.fn(); } -describe("Install Command", () => { +describe('Install Command', () => { let commandInstance: MockedInstallCommand; let config: IConfig; - const { HTTP: http } = require("http-call"); + const { HTTP: http } = require('http-call'); beforeEach(() => { mockFs.existsSync.mockReturnValue(true); config = { - name: "test", - version: "1.0.0", - channel: "stable", - cacheDir: "", - commandIDs: [""], + name: 'test', + version: '1.0.0', + channel: 'stable', + cacheDir: '', + commandIDs: [''], runHook: jest.fn(), topics: [], valid: true, - arch: "arm64", - platform: "darwin", + arch: 'arm64', + platform: 'darwin', plugins: [], commands: [], - configDir: "", - dataDir: "", - root: "", - bin: "cli", - binPath: "cli", - pjson: { oclif: { update: { s3: "./folder" } } }, + configDir: '', + dataDir: '', + root: '', + bin: 'cli', + binPath: 'cli', + pjson: { oclif: { update: { s3: './folder' } } }, scopedEnvVar: jest.fn(), scopedEnvVarKey: jest.fn(), scopedEnvVarTrue: jest.fn(), @@ -65,66 +65,66 @@ describe("Install Command", () => { } as any; }); - it("when requesting a channel, will fetch manifest to install the latest version", async () => { + it('when requesting a channel, will fetch manifest to install the latest version', async () => { mockFs.readdirSync.mockReturnValue([] as any); - commandInstance = new MockedInstallCommand(["next"], config); + commandInstance = new MockedInstallCommand(['next'], config); http.get.mockResolvedValue({ body: { - version: "1.0.0", - baseDir: "test-cli", - channel: "next", - gz: "https://test-cli-oclif.s3.amazonaws.com/test-cli-v1.0.0/test-cli-v1.0.0.tar.gz", + version: '1.0.0', + baseDir: 'test-cli', + channel: 'next', + gz: 'https://test-cli-oclif.s3.amazonaws.com/test-cli-v1.0.0/test-cli-v1.0.0.tar.gz', }, }); await commandInstance.run(); expect(commandInstance.downloadAndExtract).toBeCalled(); - expect(commandInstance.updatedVersion).toBe("next"); + expect(commandInstance.updatedVersion).toBe('next'); }); - it("when requesting a version, will return the explicit version with appropriate URL", async () => { + it('when requesting a version, will return the explicit version with appropriate URL', async () => { mockFs.readdirSync.mockReturnValue([] as any); - commandInstance = new MockedInstallCommand(["2.2.1-next.22"], config); + commandInstance = new MockedInstallCommand(['2.2.1-next.22'], config); http.get.mockResolvedValue({ body: { - version: "2.2.1-next.22", - baseDir: "test-cli", - channel: "next", - gz: "https://test-cli-oclif.s3.amazonaws.com/test-cli-v2.2.1-next.22/test-cli-v2.2.1-next.22.tar.gz", + version: '2.2.1-next.22', + baseDir: 'test-cli', + channel: 'next', + gz: 'https://test-cli-oclif.s3.amazonaws.com/test-cli-v2.2.1-next.22/test-cli-v2.2.1-next.22.tar.gz', }, }); await commandInstance.run(); expect(commandInstance.downloadAndExtract).toBeCalled(); - expect(commandInstance.updatedVersion).toBe("2.2.1-next.22"); + expect(commandInstance.updatedVersion).toBe('2.2.1-next.22'); }); - it("when requesting a version already available locally, will call updateToExistingVersion", async () => { + it('when requesting a version already available locally, will call updateToExistingVersion', async () => { mockFs.readdirSync.mockReturnValue([ - "1.0.0-next.2", - "1.0.0-next.3", - "1.0.1", - "1.0.0-alpha.0", + '1.0.0-next.2', + '1.0.0-next.3', + '1.0.1', + '1.0.0-alpha.0', ] as any); - commandInstance = new MockedInstallCommand(["1.0.0-next.3"], config); + commandInstance = new MockedInstallCommand(['1.0.0-next.3'], config); await commandInstance.run(); expect(commandInstance.updateToExistingVersion).toBeCalled(); expect(commandInstance.downloadAndExtract).not.toBeCalled(); - expect(commandInstance.updatedVersion).toBe("1.0.0-next.3"); + expect(commandInstance.updatedVersion).toBe('1.0.0-next.3'); }); - it("will handle an invalid version request", async () => { + it('will handle an invalid version request', async () => { mockFs.readdirSync.mockReturnValue([] as any); - commandInstance = new MockedInstallCommand(["2.2.1"], { + commandInstance = new MockedInstallCommand(['2.2.1'], { ...config, scopedEnvVarTrue: () => false, }); - http.get.mockRejectedValue(new Error("unable to find version")); + http.get.mockRejectedValue(new Error('unable to find version')); let err; @@ -134,12 +134,12 @@ describe("Install Command", () => { err = error; } - expect(err.message).toBe("unable to find version"); + expect(err.message).toBe('unable to find version'); }); - it("will handle an invalid channel request", async () => { + it('will handle an invalid channel request', async () => { mockFs.readdirSync.mockReturnValue([] as any); - commandInstance = new MockedInstallCommand(["2.2.1"], { + commandInstance = new MockedInstallCommand(['2.2.1'], { ...config, scopedEnvVarTrue: () => true, }); @@ -155,6 +155,6 @@ describe("Install Command", () => { } expect(commandInstance.downloadAndExtract).not.toBeCalled(); - expect(err.message).toBe("HTTP 403: Invalid channel undefined"); + expect(err.message).toBe('HTTP 403: Invalid channel undefined'); }); });