diff --git a/.github/workflows/policy-scan.yml b/.github/workflows/policy-scan.yml index 13bd362..ff25923 100644 --- a/.github/workflows/policy-scan.yml +++ b/.github/workflows/policy-scan.yml @@ -24,4 +24,23 @@ jobs: - uses: actions/checkout@master - name: Checks for License file run: | - if ! [[ -f "LICENSE" || -f "License.txt" || -f "LICENSE.md" ]]; then exit 1; fi \ No newline at end of file + expected_license_files=("LICENSE" "LICENSE.txt" "LICENSE.md" "License.txt") + license_file_found=false + current_year=$(date +"%Y") + + for license_file in "${expected_license_files[@]}"; do + if [ -f "$license_file" ]; then + license_file_found=true + # check the license file for the current year, if not exists, exit with error + if ! grep -q "$current_year" "$license_file"; then + echo "License file $license_file does not contain the current year." + exit 2 + fi + break + fi + done + + if [ "$license_file_found" = false ]; then + echo "No license file found. Please add a license file to the repository." + exit 1 + fi \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index caa16e7..52855e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@contentstack/cli-launch", - "version": "1.9.0", + "version": "1.9.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@contentstack/cli-launch", - "version": "1.9.0", + "version": "1.9.1", "license": "MIT", "dependencies": { "@apollo/client": "^3.11.8", diff --git a/package.json b/package.json index 95d0e9a..855a00a 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@contentstack/cli-launch", - "version": "1.9.0", + "version": "1.9.1", "description": "Launch related operations", "author": "Contentstack CLI", "bin": { diff --git a/src/adapters/base-class.test.ts b/src/adapters/base-class.test.ts index 2012622..c036a89 100644 --- a/src/adapters/base-class.test.ts +++ b/src/adapters/base-class.test.ts @@ -1,7 +1,6 @@ import BaseClass from './base-class'; import { cliux as ux, ContentstackClient } from '@contentstack/cli-utilities'; import config from '../config'; -import exp from 'constants'; jest.mock('@contentstack/cli-utilities', () => ({ cliux: { diff --git a/src/adapters/base-class.ts b/src/adapters/base-class.ts index 34f74b3..96f93d7 100755 --- a/src/adapters/base-class.ts +++ b/src/adapters/base-class.ts @@ -18,7 +18,6 @@ import { ApolloClient } from '@apollo/client/core'; import { writeFileSync, existsSync, readFileSync } from 'fs'; import { cliux as ux, ContentstackClient } from '@contentstack/cli-utilities'; -import config from '../config'; import { print, GraphqlApiClient, LogPolling, getOrganizations } from '../util'; import { branchesQuery, @@ -31,7 +30,6 @@ import { import { LogFn, ExitFn, - Providers, ConfigType, AdapterConstructorInputs, EmitMessage, @@ -148,31 +146,6 @@ export default class BaseClass { await this.initApolloClient(); } - /** - * @method selectProjectType - select project type/provider/adapter - * - * @return {*} {Promise} - * @memberof BaseClass - */ - async selectProjectType(): Promise { - const choices = [ - ...map(config.supportedAdapters, (provider) => ({ - value: provider, - name: `Continue with ${provider}`, - })), - { value: 'FileUpload', name: 'Continue with FileUpload' }, - ]; - - const selectedProvider: Providers = await ux.inquire({ - choices: choices, - type: 'search-list', - name: 'projectType', - message: 'Choose a project type to proceed', - }); - - this.config.provider = selectedProvider; - } - /** * @method detectFramework - detect the project framework * @@ -427,7 +400,6 @@ export default class BaseClass { * @memberof BaseClass */ async connectToAdapterOnUi(emit = true): Promise { - await this.selectProjectType(); if (includes(this.config.supportedAdapters, this.config.provider)) { const baseUrl = this.config.host.startsWith('http') ? this.config.host : `https://${this.config.host}`; @@ -862,4 +834,4 @@ export default class BaseClass { }); } } -} \ No newline at end of file +} diff --git a/src/adapters/github.test.ts b/src/adapters/github.test.ts new file mode 100644 index 00000000..6fa2171 --- /dev/null +++ b/src/adapters/github.test.ts @@ -0,0 +1,289 @@ +import GitHub from './github'; +import { getRemoteUrls } from '../util/create-git-meta'; +import { repositoriesQuery, userConnectionsQuery } from '../graphql'; +import BaseClass from './base-class'; +import { existsSync } from 'fs'; + +jest.mock('../util/create-git-meta'); +jest.mock('fs', () => ({ + ...jest.requireActual('fs'), + existsSync: jest.fn(), +})); + +const userConnections = [ + { + __typename: 'UserConnection', + userUid: 'testuser1', + provider: 'GitHub', + }, +]; +const repositories = [ + { + __typename: 'GitRepository', + id: '495370701', + url: 'https://github.com/test-user/nextjs-ssr-isr-demo', + name: 'nextjs-ssr-isr-demo', + fullName: 'test-user/nextjs-ssr-isr-demo', + defaultBranch: 'main', + }, + { + __typename: 'GitRepository', + id: '555341263', + url: 'https://github.com/test-user/static-site-demo', + name: 'static-site-demo', + fullName: 'test-user/static-site-demo', + defaultBranch: 'main', + }, + { + __typename: 'GitRepository', + id: '647250661', + url: 'https://github.com/test-user/eleventy-sample', + name: 'eleventy-sample', + fullName: 'test-user/eleventy-sample', + defaultBranch: 'main', + }, +]; + +describe('GitHub Adapter', () => { + let logMock: jest.Mock; + let exitMock: jest.Mock; + + beforeEach(() => { + logMock = jest.fn(); + exitMock = jest.fn().mockImplementationOnce(() => { + throw new Error('1'); + }); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('checkGitHubConnected', () => { + it('should return true if GitHub is connected', async () => { + const userConnectionResponse = { data: { userConnections } }; + const apolloClient = { + query: jest.fn().mockResolvedValueOnce(userConnectionResponse), + } as any; + const githubAdapterInstance = new GitHub({ + config: { projectBasePath: '/home/project1', provider: 'GitHub' }, + apolloClient: apolloClient, + log: logMock, + } as any); + const connectToAdapterOnUiMock = jest + .spyOn(BaseClass.prototype, 'connectToAdapterOnUi') + .mockResolvedValueOnce(undefined); + + await githubAdapterInstance.checkGitHubConnected(); + + expect(apolloClient.query).toHaveBeenCalledWith({ query: userConnectionsQuery }); + expect(logMock).toHaveBeenCalledWith('GitHub connection identified!', 'info'); + expect(githubAdapterInstance.config.userConnection).toEqual(userConnections[0]); + expect(connectToAdapterOnUiMock).not.toHaveBeenCalled(); + }); + + it('should log an error and exit if GitHub is not connected', async () => { + const userConnectionResponse = { data: { userConnections: [] } }; + const connectToAdapterOnUiMock = jest.spyOn(BaseClass.prototype, 'connectToAdapterOnUi').mockResolvedValueOnce(); + const apolloClient = { + query: jest.fn().mockResolvedValueOnce(userConnectionResponse), + } as any; + const githubAdapterInstance = new GitHub({ + config: { projectBasePath: '/home/project1' }, + apolloClient: apolloClient, + log: logMock, + } as any); + + await githubAdapterInstance.checkGitHubConnected(); + + expect(apolloClient.query).toHaveBeenCalledWith({ query: userConnectionsQuery }); + expect(logMock).toHaveBeenCalledWith('GitHub connection not found!', 'error'); + expect(connectToAdapterOnUiMock).toHaveBeenCalled(); + expect(githubAdapterInstance.config.userConnection).toEqual(undefined); + }); + }); + + describe('checkGitRemoteAvailableAndValid', () => { + const repositoriesResponse = { data: { repositories } }; + + it(`should successfully check if the git remote is available and valid + when the github remote URL is HTTPS based`, async () => { + (existsSync as jest.Mock).mockReturnValueOnce(true); + (getRemoteUrls as jest.Mock).mockResolvedValueOnce({ + origin: 'https://github.com/test-user/eleventy-sample.git', + }); + const apolloClient = { + query: jest.fn().mockResolvedValueOnce(repositoriesResponse), + } as any; + const githubAdapterInstance = new GitHub({ + config: { projectBasePath: '/home/project1' }, + apolloClient: apolloClient, + } as any); + + const result = await githubAdapterInstance.checkGitRemoteAvailableAndValid(); + + expect(existsSync).toHaveBeenCalledWith('/home/project1/.git'); + expect(getRemoteUrls as jest.Mock).toHaveBeenCalledWith('/home/project1/.git/config'); + expect(apolloClient.query).toHaveBeenCalledWith({ query: repositoriesQuery }); + expect(githubAdapterInstance.config.repository).toEqual({ + __typename: 'GitRepository', + id: '647250661', + url: 'https://github.com/test-user/eleventy-sample', + name: 'eleventy-sample', + fullName: 'test-user/eleventy-sample', + defaultBranch: 'main', + }); + expect(result).toBe(true); + }); + + it(`should successfully check if the git remote is available and valid + when the github remote URL is SSH based`, async () => { + (existsSync as jest.Mock).mockReturnValueOnce(true); + (getRemoteUrls as jest.Mock).mockResolvedValueOnce({ + origin: 'git@github.com:test-user/eleventy-sample.git', + }); + const apolloClient = { + query: jest.fn().mockResolvedValueOnce(repositoriesResponse), + } as any; + const githubAdapterInstance = new GitHub({ + config: { projectBasePath: '/home/project1' }, + apolloClient: apolloClient, + } as any); + + const result = await githubAdapterInstance.checkGitRemoteAvailableAndValid(); + + expect(existsSync).toHaveBeenCalledWith('/home/project1/.git'); + expect(getRemoteUrls as jest.Mock).toHaveBeenCalledWith('/home/project1/.git/config'); + expect(apolloClient.query).toHaveBeenCalledWith({ query: repositoriesQuery }); + expect(githubAdapterInstance.config.repository).toEqual({ + __typename: 'GitRepository', + id: '647250661', + url: 'https://github.com/test-user/eleventy-sample', + name: 'eleventy-sample', + fullName: 'test-user/eleventy-sample', + defaultBranch: 'main', + }); + expect(result).toBe(true); + }); + + it('should log an error and exit if git config file does not exists', async () => { + (existsSync as jest.Mock).mockReturnValueOnce(false); + const githubAdapterInstance = new GitHub({ + config: { projectBasePath: '/home/project1' }, + log: logMock, + exit: exitMock, + } as any); + let err; + + try { + await githubAdapterInstance.checkGitRemoteAvailableAndValid(); + } catch (error: any) { + err = error; + } + + expect(getRemoteUrls as jest.Mock).not.toHaveBeenCalled(); + expect(logMock).toHaveBeenCalledWith('No Git repository configuration found at /home/project1.', 'error'); + expect(logMock).toHaveBeenCalledWith( + 'Please initialize a Git repository and try again, or use the File Upload option.', + 'error', + ); + expect(exitMock).toHaveBeenCalledWith(1); + expect(err).toEqual(new Error('1')); + }); + + it(`should log an error if git repo remote url + is unavailable and exit`, async () => { + (existsSync as jest.Mock).mockReturnValueOnce(true); + (getRemoteUrls as jest.Mock).mockResolvedValueOnce(undefined); + const githubAdapterInstance = new GitHub({ + config: { projectBasePath: '/home/project1' }, + log: logMock, + exit: exitMock, + } as any); + let err; + + try { + await githubAdapterInstance.checkGitRemoteAvailableAndValid(); + } catch (error: any) { + err = error; + } + + expect(existsSync).toHaveBeenCalledWith('/home/project1/.git'); + expect(getRemoteUrls as jest.Mock).toHaveBeenCalledWith('/home/project1/.git/config'); + expect(logMock).toHaveBeenCalledWith( + `No Git remote origin URL found for the repository at /home/project1. + Please add a git remote origin url and try again`, + 'error', + ); + expect(exitMock).toHaveBeenCalledWith(1); + expect(err).toEqual(new Error('1')); + expect(githubAdapterInstance.config.repository).toBeUndefined(); + }); + + it('should log an error and exit if GitHub app is uninstalled', async () => { + (existsSync as jest.Mock).mockReturnValueOnce(true); + (getRemoteUrls as jest.Mock).mockResolvedValueOnce({ + origin: 'https://github.com/test-user/eleventy-sample.git', + }); + const apolloClient = { + query: jest.fn().mockRejectedValue(new Error('GitHub app error')), + } as any; + const connectToAdapterOnUiMock = jest.spyOn(BaseClass.prototype, 'connectToAdapterOnUi').mockResolvedValueOnce(); + const githubAdapterInstance = new GitHub({ + config: { projectBasePath: '/home/project1' }, + apolloClient: apolloClient, + log: logMock, + exit: exitMock, + } as any); + let err; + + try { + await githubAdapterInstance.checkGitRemoteAvailableAndValid(); + } catch (error: any) { + err = error; + } + + expect(existsSync).toHaveBeenCalledWith('/home/project1/.git'); + expect(getRemoteUrls as jest.Mock).toHaveBeenCalledWith('/home/project1/.git/config'); + expect(apolloClient.query).toHaveBeenCalled(); + expect(connectToAdapterOnUiMock).toHaveBeenCalled(); + expect(logMock).toHaveBeenCalledWith('GitHub app uninstalled. Please reconnect the app and try again', 'error'); + expect(exitMock).toHaveBeenCalledWith(1); + expect(err).toEqual(new Error('1')); + }); + + it('should log an error and exit if repository is not found in the list of available repositories', async () => { + (existsSync as jest.Mock).mockReturnValueOnce(true); + (getRemoteUrls as jest.Mock).mockResolvedValueOnce({ + origin: 'https://github.com/test-user/test-repo-2.git', + }); + const apolloClient = { + query: jest.fn().mockResolvedValueOnce(repositoriesResponse), + } as any; + const githubAdapterInstance = new GitHub({ + config: { projectBasePath: '/home/project1' }, + log: logMock, + exit: exitMock, + apolloClient: apolloClient, + } as any); + let err; + + try { + await githubAdapterInstance.checkGitRemoteAvailableAndValid(); + } catch (error: any) { + err = error; + } + + expect(existsSync).toHaveBeenCalledWith('/home/project1/.git'); + expect(getRemoteUrls as jest.Mock).toHaveBeenCalledWith('/home/project1/.git/config'); + expect(apolloClient.query).toHaveBeenCalledWith({ query: repositoriesQuery }); + expect(logMock).toHaveBeenCalledWith( + 'Repository not added to the GitHub app. Please add it to the app’s repository access list and try again.', + 'error', + ); + expect(exitMock).toHaveBeenCalledWith(1); + expect(err).toEqual(new Error('1')); + expect(githubAdapterInstance.config.repository).toBeUndefined(); + }); + }); +}); diff --git a/src/adapters/github.ts b/src/adapters/github.ts index 9be46b7..afe0af7 100755 --- a/src/adapters/github.ts +++ b/src/adapters/github.ts @@ -4,7 +4,6 @@ import omit from 'lodash/omit'; import find from 'lodash/find'; import split from 'lodash/split'; import { exec } from 'child_process'; -import replace from 'lodash/replace'; import includes from 'lodash/includes'; import { configHandler, cliux as ux } from '@contentstack/cli-utilities'; @@ -13,6 +12,7 @@ import BaseClass from './base-class'; import { getRemoteUrls } from '../util/create-git-meta'; import { repositoriesQuery, userConnectionsQuery, importProjectMutation } from '../graphql'; import { DeploymentStatus } from '../types'; +import { existsSync } from 'fs'; export default class GitHub extends BaseClass { /** @@ -31,14 +31,14 @@ export default class GitHub extends BaseClass { this.prepareLaunchConfig(); await this.showLogs(); - if(this.config.currentDeploymentStatus === DeploymentStatus.FAILED) { + if (this.config.currentDeploymentStatus === DeploymentStatus.FAILED) { this.exit(1); } this.showDeploymentUrl(); this.showSuggestion(); } - private async handleExistingProject(environmentUid:string): Promise { + private async handleExistingProject(environmentUid: string): Promise { await this.initApolloClient(); const redeployLastUpload = this.config['redeploy-last-upload']; @@ -49,7 +49,7 @@ export default class GitHub extends BaseClass { this.exit(1); } - if(!redeployLatest && !redeployLastUpload){ + if (!redeployLatest && !redeployLastUpload) { await this.confirmLatestRedeployment(); } @@ -260,13 +260,29 @@ export default class GitHub extends BaseClass { this.log('GitHub connection identified!', 'info'); this.config.userConnection = userConnection; } else { - this.log('GitHub connection not found!', 'warn'); + this.log('GitHub connection not found!', 'error'); await this.connectToAdapterOnUi(); } return this.config.userConnection; } + private extractRepoFullNameFromGithubRemoteURL(url: string) { + let match; + + // HTTPS format: https://github.com/owner/repo.git + match = url.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)(\.git)?$/); + if (match) { + return `${match[1]}/${match[2].replace(/\.git$/, '')}`; + } + + // SSH format: git@github.com:owner/repo.git + match = url.match(/^git@github\.com:([^/]+)\/([^/]+)(\.git)?$/); + if (match) { + return `${match[1]}/${match[2].replace(/\.git$/, '')}`; + } + } + /** * @method checkGitRemoteAvailableAndValid - GitHub repository verification * @@ -274,24 +290,46 @@ export default class GitHub extends BaseClass { * @memberof GitHub */ async checkGitRemoteAvailableAndValid(): Promise { - const localRemoteUrl = (await getRemoteUrls(resolve(this.config.projectBasePath, '.git/config')))?.origin || ''; + const gitConfigExists = this.checkGitConfigExists(); + if (!gitConfigExists) { + this.log(`No Git repository configuration found at ${this.config.projectBasePath}.`, 'error'); + this.log('Please initialize a Git repository and try again, or use the File Upload option.', 'error'); + this.exit(1); + } + const gitConfigFilePath = resolve(this.config.projectBasePath, '.git/config'); + const remoteUrls = await getRemoteUrls(gitConfigFilePath); + const localRemoteUrl = remoteUrls?.origin || ''; if (!localRemoteUrl) { - this.log('GitHub project not identified!', 'error'); + this.log( + `No Git remote origin URL found for the repository at ${this.config.projectBasePath}. + Please add a git remote origin url and try again`, + 'error', + ); + this.exit(1); + } + + let repositories; + try { + const repositoriesQueryResponse = await this.apolloClient.query({ query: repositoriesQuery }); + repositories = repositoriesQueryResponse.data.repositories; + } catch { + this.log('GitHub app uninstalled. Please reconnect the app and try again', 'error'); await this.connectToAdapterOnUi(); + this.exit(1); } - const repositories = await this.apolloClient - .query({ query: repositoriesQuery }) - .then(({ data: { repositories } }) => repositories) - .catch((error) => this.log(error, 'error')); + const repoFullName = this.extractRepoFullNameFromGithubRemoteURL(localRemoteUrl); this.config.repository = find(repositories, { - url: replace(localRemoteUrl, '.git', ''), + fullName: repoFullName, }); if (!this.config.repository) { - this.log('Repository not found in the list!', 'error'); + this.log( + 'Repository not added to the GitHub app. Please add it to the app’s repository access list and try again.', + 'error', + ); this.exit(1); } @@ -306,6 +344,7 @@ export default class GitHub extends BaseClass { */ async checkUserGitHubAccess(): Promise { return new Promise((resolve, reject) => { + // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; const defaultBranch = this.config.repository?.defaultBranch; if (!defaultBranch) return reject('Branch not found'); @@ -331,4 +370,9 @@ export default class GitHub extends BaseClass { ); }); } + + private checkGitConfigExists(): boolean { + const gitExists = existsSync(resolve(this.config.projectBasePath, '.git')); + return gitExists; + } } diff --git a/src/adapters/pre-check.ts b/src/adapters/pre-check.ts index b5e51e0..addc429 100755 --- a/src/adapters/pre-check.ts +++ b/src/adapters/pre-check.ts @@ -1,12 +1,9 @@ import find from 'lodash/find'; -import { resolve } from 'path'; import { existsSync } from 'fs'; import isEmpty from 'lodash/isEmpty'; -import includes from 'lodash/includes'; import { cliux as ux } from '@contentstack/cli-utilities'; import BaseClass from './base-class'; -import { getRemoteUrls } from '../util'; export default class PreCheck extends BaseClass { public projectBasePath: string = process.cwd(); @@ -17,12 +14,8 @@ export default class PreCheck extends BaseClass { * @return {*} {Promise} * @memberof PreCheck */ - async run(identifyProject = true): Promise { + async run(): Promise { await this.performValidations(); - - if (identifyProject && !this.config.isExistingProject) { - await this.identifyWhatProjectItIs(); - } } /** @@ -108,30 +101,4 @@ export default class PreCheck extends BaseClass { } } catch (error) {} } - - /** - * @method identifyWhatProjectItIs - identify if the project type (is GitHub, BitBucket, FileUpload etc.,) - * - * @return {*} {Promise} - * @memberof PreCheck - */ - async identifyWhatProjectItIs(): Promise { - const localRemoteUrl = (await getRemoteUrls(resolve(this.config.projectBasePath, '.git/config')))?.origin || ''; - - switch (true) { - case includes(localRemoteUrl, 'github.'): - this.config.provider = 'GitHub'; - this.log('Git project identified', 'info'); - break; - default: - if (existsSync(resolve(this.config.projectBasePath, '.git'))) { - this.log('Git config found but remote URL not found in the config!', { - color: 'yellow', - bold: true, - }); - } - await this.connectToAdapterOnUi(false); - break; - } - } } diff --git a/src/commands/launch/index.test.ts b/src/commands/launch/index.test.ts new file mode 100644 index 00000000..c5f0547 --- /dev/null +++ b/src/commands/launch/index.test.ts @@ -0,0 +1,92 @@ +import Launch from './index'; +import { BaseCommand } from '../../base-command'; +import { FileUpload, GitHub, PreCheck } from '../../adapters'; +import { cliux } from '@contentstack/cli-utilities'; + +jest.mock('@contentstack/cli-utilities'); + +jest.mock('../../base-command'); + +describe('Run', () => { + let launchCommandInstance: Launch; + let prepareApiClientsMock: jest.SpyInstance; + let preCheckRunMock: jest.SpyInstance; + + beforeEach(() => { + prepareApiClientsMock = jest.spyOn(BaseCommand.prototype, 'prepareApiClients').mockResolvedValueOnce(undefined); + // @ts-expect-error - Override readonly property context on BaseCommand for testing + BaseCommand.prototype['context'] = { analyticsInfo: {} } as any; + BaseCommand.prototype['$event'] = { on: jest.fn() } as any; + preCheckRunMock = jest.spyOn(PreCheck.prototype, 'run').mockResolvedValueOnce(undefined); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should successfully run launch command for provider GitHub', async () => { + const githubRunMock = jest.spyOn(GitHub.prototype, 'run').mockResolvedValueOnce(undefined); + BaseCommand.prototype['sharedConfig'] = { provider: 'GitHub', isExistingProject: true } as any; + BaseCommand.prototype['flags'] = { init: false }; + launchCommandInstance = new Launch([], {} as any); + + await launchCommandInstance.run(); + + expect(prepareApiClientsMock).toHaveBeenCalled(); + expect(preCheckRunMock).toHaveBeenCalled(); + expect(githubRunMock).toHaveBeenCalled(); + }); + + it('should successfully run launch command for provider FileUpload', async () => { + const fileUploadRunMock = jest.spyOn(FileUpload.prototype, 'run').mockResolvedValueOnce(undefined); + BaseCommand.prototype['sharedConfig'] = { provider: 'FileUpload', isExistingProject: true } as any; + BaseCommand.prototype['flags'] = { init: false }; + launchCommandInstance = new Launch([], {} as any); + + await launchCommandInstance.run(); + + expect(prepareApiClientsMock).toHaveBeenCalled(); + expect(preCheckRunMock).toHaveBeenCalled(); + expect(fileUploadRunMock).toHaveBeenCalled(); + }); + + it('should successfully run launch command for other providers', async () => { + const connectToAdapterOnUiMock = jest + .spyOn(PreCheck.prototype, 'connectToAdapterOnUi') + .mockResolvedValueOnce(undefined); + BaseCommand.prototype['sharedConfig'] = { provider: 'OtherProvider', isExistingProject: true } as any; + BaseCommand.prototype['flags'] = { init: false }; + launchCommandInstance = new Launch([], {} as any); + + await launchCommandInstance.run(); + + expect(prepareApiClientsMock).toHaveBeenCalled(); + expect(preCheckRunMock).toHaveBeenCalled(); + expect(connectToAdapterOnUiMock).toHaveBeenCalled(); + }); + + it(`should successfully run launch command when its a new project, + and prompt to select the project type`, + async () => { + const githubRunMock = jest.spyOn(GitHub.prototype, 'run').mockResolvedValueOnce(undefined); + BaseCommand.prototype['sharedConfig'] = { provider: 'GitHub', isExistingProject: false } as any; + BaseCommand.prototype['flags'] = { init: false }; + (cliux.inquire as jest.Mock).mockResolvedValueOnce('GitHub'); + launchCommandInstance = new Launch([], {} as any); + + await launchCommandInstance.run(); + + expect(prepareApiClientsMock).toHaveBeenCalled(); + expect(preCheckRunMock).toHaveBeenCalled(); + expect(githubRunMock).toHaveBeenCalled(); + expect(cliux.inquire).toHaveBeenCalledWith({ + choices: [ + { name: 'Continue with GitHub', value: 'GitHub' }, + { name: 'Continue with FileUpload', value: 'FileUpload' }, + ], + type: 'search-list', + name: 'projectType', + message: 'Choose a project type to proceed', + }); + }); +}); diff --git a/src/commands/launch/index.ts b/src/commands/launch/index.ts index bca8bd8..b2fe6b4 100755 --- a/src/commands/launch/index.ts +++ b/src/commands/launch/index.ts @@ -1,10 +1,10 @@ import map from 'lodash/map'; -import { FlagInput, Flags } from '@contentstack/cli-utilities'; import config from '../../config'; import { BaseCommand } from '../../base-command'; -import { AdapterConstructorInputs } from '../../types'; +import { AdapterConstructorInputs, Providers } from '../../types'; import { FileUpload, GitHub, PreCheck } from '../../adapters'; +import { FlagInput, Flags, cliux } from '@contentstack/cli-utilities'; export default class Launch extends BaseCommand { public preCheck!: PreCheck; @@ -102,6 +102,9 @@ export default class Launch extends BaseCommand { // NOTE pre-check: manage flow and set the provider value await this.preCheckAndInitConfig(); + if (!this.sharedConfig.isExistingProject) { + await this.selectProjectType(); + } await this.manageFlowBasedOnProvider(); } @@ -123,15 +126,15 @@ export default class Launch extends BaseCommand { }; switch (this.sharedConfig.provider) { - case 'GitHub': - await new GitHub(adapterConstructorInputs).run(); - break; - case 'FileUpload': - await new FileUpload(adapterConstructorInputs).run(); - break; - default: - await this.preCheck.connectToAdapterOnUi(); - break; + case 'GitHub': + await new GitHub(adapterConstructorInputs).run(); + break; + case 'FileUpload': + await new FileUpload(adapterConstructorInputs).run(); + break; + default: + await this.preCheck.connectToAdapterOnUi(); + break; } } @@ -151,7 +154,32 @@ export default class Launch extends BaseCommand { managementSdk: this.managementSdk, analyticsInfo: this.context.analyticsInfo, }); - - await this.preCheck.run(!this.flags.type); + + await this.preCheck.run(); + } + + /** + * @method selectProjectType - select project type/provider/adapter + * + * @return {*} {Promise} + * @memberof BaseClass + */ + private async selectProjectType(): Promise { + const choices = [ + ...map(config.supportedAdapters, (provider) => ({ + value: provider, + name: `Continue with ${provider}`, + })), + { value: 'FileUpload', name: 'Continue with FileUpload' }, + ]; + + const selectedProvider: Providers = await cliux.inquire({ + choices: choices, + type: 'search-list', + name: 'projectType', + message: 'Choose a project type to proceed', + }); + + this.sharedConfig.provider = selectedProvider; } }