diff --git a/src/commands/install.ts b/src/commands/install.ts new file mode 100644 index 00000000..635fa861 --- /dev/null +++ b/src/commands/install.ts @@ -0,0 +1,52 @@ +import cli from 'cli-ux' +import * as semver from 'semver' +import * as fs from 'fs-extra' + +import UpdateCommand from './update' + +export default class InstallCommand extends UpdateCommand { + static args = [{name: 'version', optional: false}] + + static flags = {} + + async run() { + const {args} = this.parse(InstallCommand) + + // Check if this command is trying to update the channel. TODO: make this dynamic + const channelUpdateRequested = ['alpha', 'beta', 'next', 'stable'].some(c => args.version === c) + this.channel = channelUpdateRequested ? args.version : await this.determineChannel() + const versions = fs + .readdirSync(this.clientRoot) + .filter(dirOrFile => dirOrFile !== 'bin' && dirOrFile !== 'current') + + const targetVersion = semver.clean(args.version) || args.version + + if (versions.includes(targetVersion)) { + await this.updateToExistingVersion(targetVersion) + this.currentVersion = await this.determineCurrentVersion() + this.updatedVersion = targetVersion + if (channelUpdateRequested) { + await this.setChannel() + } + } else { + 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) + this.debug('tidy') + await this.tidy() + await this.config.runHook('update', {channel: this.channel}) + + this.debug('done') + cli.action.stop() + } + + + } +} diff --git a/test/commands/install.test.ts b/test/commands/install.test.ts index 851153e0..7247b986 100644 --- a/test/commands/install.test.ts +++ b/test/commands/install.test.ts @@ -1,28 +1,50 @@ -import InstallCommand from '../../src/commands/use'; -import * as fs from 'fs'; -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' -const mockFs = mocked(fs, true); +jest.mock('fs-extra') +jest.mock('http-call', () => ({ + HTTP: { + get: jest.fn(), + }, +})) +const mockFs = mocked(fs, true) class MockedInstallCommand extends InstallCommand { - public fetchManifest = jest.fn(); + public channel!: string; + + public clientBin!: string; + + public clientRoot!: string; + + public currentVersion!: string; + + public updatedVersion!: string; + + public determineCurrentVersion = jest.fn(); public downloadAndExtract = jest.fn(); + + public reexec = jest.fn(); + + public updateToExistingVersion = jest.fn(); } -describe.skip('Install Command', () => { - let commandInstance: MockedInstallCommand; - let config: IConfig; +describe('Install Command', () => { + let commandInstance: MockedInstallCommand + let config: IConfig + const {HTTP: http} = require('http-call') beforeEach(() => { mockFs.existsSync.mockReturnValue(true); config = { name: 'test', - version: '', - channel: '', + version: '1.0.0', + channel: 'stable', cacheDir: '', commandIDs: [''], + runHook: jest.fn(), topics: [], valid: true, arch: 'arm64', @@ -30,24 +52,111 @@ describe.skip('Install Command', () => { plugins: [], commands: [], configDir: '', - pjson: {} as any, + dataDir: '', root: '', - bin: '', - } as any; - }); - - it.skip('will run an update', async () => { - commandInstance = new MockedInstallCommand([], config); - - await commandInstance.run(); - }); - - it.todo( - 'when requesting a channel, will fetch manifest to install the latest version', - ); - it.todo( - 'when requesting a version, will return the explicit version with appropriate URL', - ); - it.todo('will handle an invalid version request'); - it.todo('will handle an invalid channel request'); -}); + bin: 'cli', + binPath: 'cli', + pjson: {oclif: {update: {s3: './folder'}}}, + scopedEnvVar: jest.fn(), + scopedEnvVarKey: jest.fn(), + scopedEnvVarTrue: jest.fn(), + s3Url: () => null, + s3Key: jest.fn(), + } as any + }) + + it('when requesting a channel, will fetch manifest to install the latest version', async () => { + mockFs.readdirSync.mockReturnValue([] as any) + 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', + xz: 'https://test-cli-oclif.s3.amazonaws.com/test-cli-v1.0.0/test-cli-v1.0.0.tar.xz', + sha256gz: 'cae9de53d3cb9bfdb43b5fd75b1d9f4655e07cf479a8d86658155ff66d618dbb', + node: { + compatible: '>=10', + recommended: '10.24.0', + }, + }}) + + await commandInstance.run() + + expect(commandInstance.downloadAndExtract).toBeCalled() + expect(commandInstance.updatedVersion).toBe('next') + }) + + 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'], config) + + http.get.mockResolvedValue({body: { + version: '2.2.1', + baseDir: 'test-cli', + channel: 'next', + gz: 'https://test-cli-oclif.s3.amazonaws.com/test-cli-v2.2.1/test-cli-v2.2.1.tar.gz', + xz: 'https://test-cli-oclif.s3.amazonaws.com/test-cli-v2.2.1/test-cli-v2.2.1.tar.xz', + sha256gz: 'cae9de53d3cb9bfdb43b5fd75b1d9f4655e07cf479a8d86658155ff66d618dbb', + node: { + compatible: '>=10', + recommended: '10.24.0', + }, + }}) + + await commandInstance.run() + + expect(commandInstance.downloadAndExtract).toBeCalled() + expect(commandInstance.updatedVersion).toBe('2.2.1') + }) + + 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', + ] as any) + 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') + }) + + it('will handle an invalid version request', async () => { + mockFs.readdirSync.mockReturnValue([] as any) + commandInstance = new MockedInstallCommand(['2.2.1'], {...config, scopedEnvVarTrue: () => false}) + http.get.mockRejectedValue(new Error('unable to find version')) + + let err + + try { + await commandInstance.run() + } catch (error) { + err = error + } + + expect(err.message).toBe('unable to find version') + }) + + it('will handle an invalid channel request', async () => { + mockFs.readdirSync.mockReturnValue([] as any) + commandInstance = new MockedInstallCommand(['2.2.1'], {...config, scopedEnvVarTrue: () => true}) + + http.get.mockRejectedValue({statusCode: 403}) + + let err + + try { + await commandInstance.run() + } catch (error) { + err = error + } + + expect(commandInstance.downloadAndExtract).not.toBeCalled() + expect(err.message).toBe('HTTP 403: Invalid channel undefined') + }) +})