UNPKG

@sprucelabs/spruce-cli

Version:

Command line interface for building Spruce skills.

541 lines (446 loc) • 16.3 kB
import pathUtil from 'path' import globby from '@sprucelabs/globby' import { MercuryClientFactory } from '@sprucelabs/mercury-client' import { SchemaRegistry } from '@sprucelabs/schema' import { AuthService, diskUtil } from '@sprucelabs/spruce-skill-utils' import { templates } from '@sprucelabs/spruce-templates' import AbstractSpruceTest, { assert, generateId } from '@sprucelabs/test-utils' import fs from 'fs-extra' import EventFaker from '../__tests__/support/EventFaker' import AbstractAction from '../features/AbstractAction' import { FeatureOptions } from '../features/AbstractFeature' import ActionExecuter, { ActionExecuterOptions, } from '../features/ActionExecuter' import ActionFactory from '../features/ActionFactory' import EventStore from '../features/event/stores/EventStore' import FeatureInstaller from '../features/FeatureInstaller' import FeatureInstallerFactory from '../features/FeatureInstallerFactory' import { FeatureActionResponse, FeatureCode } from '../features/features.types' import OnboardingStore from '../features/onboard/stores/OnboardingStore' import SkillStoreImpl from '../features/skill/stores/SkillStore' import CliGlobalEmitter, { GlobalEmitter } from '../GlobalEmitter' import SpyInterface from '../interfaces/SpyInterface' import CommandServiceImpl from '../services/CommandService' import ImportService from '../services/ImportService' import LintService from '../services/LintService' import ServiceFactory, { Service, ServiceMap } from '../services/ServiceFactory' import StoreFactory, { StoreCode, CreateStoreOptions, StoreFactoryOptions, StoreMap, } from '../stores/StoreFactory' import { ApiClientFactoryOptions } from '../types/apiClient.types' import { CliBootOptions, OptionOverrides } from '../types/cli.types' import WriterFactory from '../writers/WriterFactory' import CommandFaker from './CommandFaker' import FeatureFixture, { FeatureFixtureOptions, } from './fixtures/FeatureFixture' import GoFixture from './fixtures/GoFixture' import MercuryFixture from './fixtures/MercuryFixture' import OrganizationFixture from './fixtures/OrganizationFixture' import PersonFixture from './fixtures/PersonFixture' import SkillFixture from './fixtures/SkillFixture' import ViewFixture from './fixtures/ViewFixture' import testUtil from './utilities/test.utility' export default abstract class AbstractCliTest extends AbstractSpruceTest { protected static cliRoot = pathUtil.join(__dirname, '..') protected static homeDir: string protected static commandFaker: CommandFaker private static _ui: SpyInterface private static _emitter?: GlobalEmitter private static mercuryFixture?: MercuryFixture private static personFixture?: PersonFixture private static organizationFixture?: OrganizationFixture private static skillFixture?: SkillFixture private static _featureInstaller?: FeatureInstaller private static viewFixture?: ViewFixture private static goFixture?: GoFixture private static originalEnv: Record<string, string | undefined> private static _writers?: WriterFactory protected static eventFaker: EventFaker protected static async beforeAll() { await super.beforeAll() await this.cleanTestDirsAndFiles() LintService.disableLinting() ServiceFactory.reset() WriterFactory.reset() ImportService.setCacheDir(diskUtil.createRandomTempDir()) ImportService.enableCaching() this.eventFaker = new EventFaker() process.env.ENABLE_INSTALL_ENTERTAINMENT = 'false' this.originalEnv = { ...process.env } } protected static async beforeEach() { await super.beforeEach() delete this._writers ActionFactory.clearActionOverrides() //@ts-ignore process.env = { ...this.originalEnv } testUtil.startLogTimer() SchemaRegistry.getInstance().forgetAllSchemas() this.cwd = this.freshTmpDir() this.homeDir = this.freshTmpDir() AuthService.homeDir = this.homeDir OnboardingStore.overrideCwd(diskUtil.createRandomTempDir()) this.ui.reset() this.ui.invocations = [] this.ui.setCursorPosition({ x: 0, y: 0 }) this.setCwd(this.cwd) ImportService.clearCache() SkillStoreImpl.clearCurrentSkill() EventStore.clearCache() CommandServiceImpl.clearFakedResponses() MercuryClientFactory.reset() MercuryClientFactory.setIsTestMode(false) this.commandFaker = new CommandFaker() } protected static async afterEach() { await super.afterEach() await this.organizationFixture?.clearAllOrgs() await this.mercuryFixture?.disconnectAll() this.clearFixtures() if (this._ui?.isWaitingForInput()) { throw new Error( `Terminal interface is waiting for input. Make sure you are invoking this.term.sendInput() as many times as needed.` ) } if (diskUtil.doesDirExist(this.cwd) && testUtil.shouldClearCache()) { diskUtil.deleteDir(this.cwd) } if ( diskUtil.doesDirExist(this.homeDir) && testUtil.shouldClearCache() ) { diskUtil.deleteDir(this.homeDir) } if (testUtil.shouldCleanupAfterEach()) { FeatureFixture.deleteOldSkillDirs() } } protected static async afterAll() { await super.afterAll() if (testUtil.shouldCleanupAfterAll()) { FeatureFixture.deleteOldSkillDirs() } } private static async cleanTestDirsAndFiles() { const pattern = this.resolveTestPath('**/*.d.ts') const matches = await globby(pattern) for (const match of matches) { diskUtil.deleteFile(match) } } protected static clearFixtures() { this._featureInstaller = undefined this._emitter = undefined this.mercuryFixture = undefined this.organizationFixture = undefined this.personFixture = undefined this.skillFixture = undefined this.viewFixture = undefined this.goFixture = undefined } protected static setCwd(cwd: string) { this.clearFixtures() this.cwd = cwd } protected static freshTmpDir() { const tmpDirectory = testUtil.resolveTestDir(generateId()) fs.ensureDirSync(tmpDirectory) return tmpDirectory } protected static get ui() { if (!this._ui) { this._ui = new SpyInterface() } return this._ui } protected static get emitter() { if (!this._emitter) { this._emitter = CliGlobalEmitter.Emitter() } return this._emitter } protected static set emitter(emitter: GlobalEmitter) { this._emitter = emitter } protected static resolveTestPath(...pathAfterTestDirsAndFiles: string[]) { return pathUtil.join( this.cliRoot, '__tests__', 'testDirsAndFiles', ...pathAfterTestDirsAndFiles ) } protected static async Cli(options?: CliBootOptions) { return this.FeatureFixture(options).Cli({ cwd: this.cwd, homeDir: this.homeDir, ...(options ?? {}), }) } protected static async linkLocalPackages() { const fixture = this.FeatureFixture() await fixture.linkLocalPackages() } protected static Service<S extends Service>( type: S, cwd?: string ): ServiceMap[S] { const sf = this.ServiceFactory() return sf.Service(cwd ?? this.cwd, type) } protected static ServiceFactory() { return new ServiceFactory() } protected static FeatureFixture(options?: Partial<FeatureFixtureOptions>) { return new FeatureFixture({ cwd: this.cwd, serviceFactory: this.ServiceFactory(), ui: this.ui, apiClientFactory: this.getMercuryFixture().getApiClientFactory(), featureInstaller: this.featureInstaller, emitter: this.emitter, ...options, }) } protected static getMercuryFixture() { if (!this.mercuryFixture) { this.mercuryFixture = new MercuryFixture( this.cwd, this.ServiceFactory() ) } return this.mercuryFixture } protected static get people() { if (!this.personFixture) { this.personFixture = new PersonFixture( this.getMercuryFixture().getApiClientFactory() ) } return this.personFixture } protected static get go() { if (!this.goFixture) { this.goFixture = new GoFixture(this.Service('command')) } return this.goFixture } protected static getViewFixture() { if (!this.viewFixture) { const viewWriter = this.writers.Writer('view', { fileDescriptions: [], }) this.viewFixture = new ViewFixture( this.cwd, viewWriter, this.Action('view', 'sync') ) } return this.viewFixture } protected static get writers() { if (!this._writers) { this._writers = this.WriterFactory() } return this._writers } private static WriterFactory() { return new WriterFactory({ templates, ui: this.ui, settings: this.Service('settings'), }) } protected static async skipInstallSkillPrompts< E extends () => Promise<FeatureActionResponse>, >( execute?: E ): Promise<E extends undefined ? undefined : FeatureActionResponse> { const promise = execute?.() await this.waitForInput() await this.ui.sendInput('n') await this.waitForInput() await this.ui.sendInput('') const results = await promise return results as any } protected static getOrganizationFixture() { if (!this.organizationFixture) { this.organizationFixture = new OrganizationFixture( this.people, this.StoreFactory() ) } return this.organizationFixture } protected static getSkillFixture() { if (!this.skillFixture) { this.skillFixture = new SkillFixture( this.people, this.StoreFactory(), this.getMercuryFixture().getApiClientFactory() ) } return this.skillFixture } protected static resolveHashSprucePath(...filePath: string[]) { return diskUtil.resolveHashSprucePath(this.cwd, ...filePath) } protected static get featureInstaller() { if (!this._featureInstaller) { const installer = this.FeatureInstaller() this._featureInstaller = installer } return this._featureInstaller } protected static set featureInstaller(installer: FeatureInstaller) { this._featureInstaller = installer } protected static FeatureInstaller(options?: Partial<FeatureOptions>) { const serviceFactory = this.ServiceFactory() const storeFactory = this.StoreFactory(options) const emitter = options?.emitter ?? this.emitter const apiClientFactory = this.getMercuryFixture().getApiClientFactory() const actionExecuter = this.ActionExecuter() const installer = FeatureInstallerFactory.WithAllFeatures({ cwd: this.cwd, serviceFactory, storeFactory, ui: this.ui, emitter, apiClientFactory, actionExecuter, ...options, }) return installer } protected static StoreFactory(options?: Partial<StoreFactoryOptions>) { const serviceFactory = this.ServiceFactory() return new StoreFactory({ cwd: this.cwd, serviceFactory, homeDir: this.homeDir, apiClientFactory: this.getMercuryFixture().getApiClientFactory(), emitter: this.emitter, ...options, }) } protected static Store<C extends StoreCode>( code: C, options?: CreateStoreOptions<C> ): StoreMap[C] { return this.StoreFactory().Store(code, { cwd: this.cwd, ...(options as any), }) } protected static async waitForInput() { return this.ui.waitForInput() } protected static async assertIsFeatureInstalled(code: FeatureCode) { const isInstalled = await this.featureInstaller.isInstalled(code) assert.isTrue(isInstalled) } protected static async assertValidActionResponseFiles( results: FeatureActionResponse ) { for (const file of results.files ?? []) { await this.assertFilePassesTypeChecks(file.path) } // await Promise.all( // (results.files ?? []).map((file) => { // const checker = this.Service('typeChecker') // return checker.check(file.path) // }) // ) } protected static async assertFilePassesTypeChecks(file: string) { const checker = this.Service('typeChecker') await checker.check(file) } protected static async connectToApi(options?: ApiClientFactoryOptions) { return this.getMercuryFixture().connectToApi(options) } protected static async openInVsCode(options?: { file?: string dir?: string timeout?: number }) { await this.Action('vscode', 'setup').execute({ all: true }) await this.Service('command').execute( `code ${options?.file ?? options?.dir ?? this.cwd}` ) await this.wait(options?.timeout ?? 99999999) } protected static async openInFinder(options?: { file?: string dir?: string timeout?: number }) { await this.Service('command').execute( `open ${options?.file ?? options?.dir ?? this.cwd}` ) await this.wait(options?.timeout ?? 99999999) } protected static log(...args: any[]) { testUtil.log(...args) } protected static Action< Action extends AbstractAction = AbstractAction, F extends FeatureCode = FeatureCode, >(featureCode: F, actionCode: string, options?: ExecuterOptions): Action { const executer = this.ActionExecuter({ shouldThrowOnListenerError: true, ...options, }).Action(featureCode, actionCode) return executer as any } protected static ActionExecuter(options?: ExecuterOptions) { const serviceFactory = this.ServiceFactory() const actionFactory = new ActionFactory({ writerFactory: this.writers, ui: this.ui, emitter: this.emitter, apiClientFactory: this.getMercuryFixture().getApiClientFactory(), cwd: this.cwd, serviceFactory, storeFactory: this.StoreFactory(), templates, optionOverrides: { 'sync.schemas': { shouldInstallMissingDependencies: true, }, ...options?.optionOverrides, }, }) const executer = new ActionExecuter({ ui: this.ui, emitter: this.emitter, actionFactory, featureInstallerFactory: () => { return this.featureInstaller }, shouldAutoHandleDependencies: false, ...options, }) return executer } protected static selectOptionBasedOnLabel(label: string) { const last = this.ui.getLastInvocation() assert.doesInclude(last.options.options.choices, { label, }) const match = last.options.options.choices.find( (o: any) => o.label === label ) void this.ui.sendInput(`${match.value}`) } } type ExecuterOptions = Partial<ActionExecuterOptions> & { optionOverrides?: OptionOverrides }