@sprucelabs/spruce-cli
Version:
Command line interface for building Spruce skills.
297 lines (246 loc) • 8.51 kB
text/typescript
import pathUtil from 'path'
import { diskUtil } from '@sprucelabs/spruce-skill-utils'
import Cli from '../../cli/Cli'
import FeatureInstaller from '../../features/FeatureInstaller'
import { InstallFeature } from '../../features/features.types'
import { GlobalEmitter } from '../../GlobalEmitter'
import ServiceFactory, {
ServiceProvider,
Service,
ServiceMap,
} from '../../services/ServiceFactory'
import { ApiClientFactory } from '../../types/apiClient.types'
import {
CliBootOptions,
CliInterface,
GraphicsInterface,
} from '../../types/cli.types'
import testUtil from '../utilities/test.utility'
export default class FeatureFixture implements ServiceProvider {
private cwd: string
private installedSkills: Record<string, CachedCli> = {}
private serviceFactory: ServiceFactory
private static linkedUtils = false
private static dirsToDelete: string[] = []
private ui: GraphicsInterface
private generateCacheIfMissing = false
private apiClientFactory: ApiClientFactory
private emitter?: GlobalEmitter
private featureInstaller?: FeatureInstaller
public constructor(options: FeatureFixtureOptions) {
if (options.cwd.search('packages/spruce-cli') > -1) {
throw new Error(
"You can't run FeatureFixture in the cli directory."
)
}
this.cwd = options.cwd
this.serviceFactory = options.serviceFactory
this.ui = options.ui
this.generateCacheIfMissing = !!options.shouldGenerateCacheIfMissing
this.apiClientFactory = options.apiClientFactory
this.emitter = options.emitter
this.featureInstaller = options.featureInstaller
}
public static deleteOldSkillDirs() {
for (const dir of this.dirsToDelete) {
try {
diskUtil.deleteDir(dir)
} catch {}
}
this.dirsToDelete = []
}
public Service<S extends Service>(
type: S,
cwd?: string | undefined
): ServiceMap[S] {
return this.serviceFactory.Service(cwd ?? this.cwd, type)
}
public async Cli(options?: CliBootOptions) {
await this.linkWorkspacePackages()
const cli = await Cli.Boot({
cwd: this.cwd,
graphicsInterface: this.ui,
apiClientFactory: this.apiClientFactory,
emitter: this.emitter,
featureInstaller: this.featureInstaller,
...(options ?? {}),
})
return cli
}
private async linkWorkspacePackages() {
if (!FeatureFixture.linkedUtils) {
FeatureFixture.linkedUtils = true
// const expectedLinkedDir = pathUtil.join(
// os.homedir(),
// '.config',
// 'yarn',
// 'link',
// '@sprucelabs',
// 'spruce-skill-utils'
// )
// if (!fsUtil.existsSync(expectedLinkedDir)) {
// const command = this.Service('command')
// try {
// await command.execute(
// `cd ${pathUtil.join(
// __dirname,
// '..',
// '..',
// '..',
// 'spruce-skill-utils'
// )} && yarn link`
// )
// } catch (err) {
// if (fsUtil.existsSync(expectedLinkedDir)) {
// log.warn(`Symlink ${expectedLinkedDir} already exists`)
// } else {
// log.warn(
// `Symlink ${expectedLinkedDir} failed, but the check thinks it is missing`
// )
// }
// }
// }
}
}
public async installCachedFeatures(
cacheKey: string,
bootOptions?: CliBootOptions
) {
return this.installFeatures([], cacheKey, bootOptions)
}
public async installFeatures(
features: InstallFeature[],
cacheKey?: string,
bootOptions?: CliBootOptions
) {
if (this.isCached(cacheKey)) {
return this.installedSkills[cacheKey as string].cli
}
let isCached = false
if (cacheKey && testUtil.isCacheEnabled()) {
isCached = this.doesCacheExist(cacheKey)
if (!isCached && !this.generateCacheIfMissing) {
throw new Error(
`Cached skill not found, make sure\n\n"${cacheKey}"\n\nis in your package.json under "testSkillCache" and run\n\n\`yarn cache.tests\``
)
}
if (isCached) {
await this.copyCachedSkillToCwd(cacheKey)
} else {
this.removeCwdFromCacheTracker(cacheKey)
}
}
const cli = await this.Cli(bootOptions)
if (!isCached) {
await cli.installFeatures({
features,
})
}
if (cacheKey && testUtil.isCacheEnabled()) {
!isCached && this.addCwdToCacheTracker(cacheKey)
this.cacheCli(cacheKey, cli)
}
await this.linkLocalPackages()
return cli
}
private isCached(cacheKey: string | undefined) {
return (
cacheKey &&
this.installedSkills[cacheKey] &&
testUtil.isCacheEnabled()
)
}
public async linkLocalPackages() {
// const command = this.Service('command')
// await command.execute(`yarn link @sprucelabs/spruce-skill-utils`)
}
private async copyCachedSkillToCwd(cacheKey: string) {
let isCached = this.doesCacheExist(cacheKey)
if (isCached) {
let settings = this.loadCacheTracker()
await diskUtil.copyDir(settings[cacheKey], this.cwd)
if (process.env.TEST_HOST) {
this.Service('env').set('HOST', process.env.TEST_HOST)
}
FeatureFixture.dirsToDelete.push(this.cwd)
}
}
private addCwdToCacheTracker(cacheKey: string) {
let settings = this.loadCacheTracker()
if (!settings) {
settings = {}
}
if (!settings[cacheKey]) {
settings[cacheKey] = this.cwd
this.writeCacheSettings(settings)
}
return settings
}
private removeCwdFromCacheTracker(cacheKey: string) {
let settings = this.loadCacheTracker()
if (!settings) {
settings = {}
}
if (settings[cacheKey]) {
delete settings[cacheKey]
this.writeCacheSettings(settings)
}
}
private writeCacheSettings(settings: Record<string, any>) {
const settingsFile = this.getTestCacheTrackerFilePath()
const settingsFolder = pathUtil.dirname(settingsFile)
!diskUtil.doesDirExist(settingsFolder) &&
diskUtil.createDir(settingsFolder)
diskUtil.writeFile(settingsFile, JSON.stringify(settings, null, 2))
}
private doesCacheExist(cacheKey: string) {
let alreadyInstalled = false
const settings = this.loadCacheTracker()
if (settings?.[cacheKey]) {
alreadyInstalled = true
}
if (alreadyInstalled) {
alreadyInstalled = diskUtil.doesDirExist(
diskUtil.resolvePath(settings[cacheKey], 'node_modules')
)
}
return alreadyInstalled
}
public loadCacheTracker() {
const settingsFile = this.getTestCacheTrackerFilePath()
const exists = diskUtil.doesFileExist(settingsFile)
let settingsObject: Record<string, any> = {}
try {
settingsObject = exists
? JSON.parse(diskUtil.readFile(settingsFile))
: {}
} catch {}
return settingsObject
}
public getTestCacheTrackerFilePath() {
return diskUtil.resolveHashSprucePath(
__dirname,
'tmp',
`cached-skills.json`
)
}
private cacheCli(cacheKey: string, cli: CliInterface) {
this.installedSkills[cacheKey] = {
cwd: this.cwd,
cli,
}
}
}
export interface CachedCli {
cli: CliInterface
cwd: string
}
export interface FeatureFixtureOptions {
cwd: string
serviceFactory: ServiceFactory
ui: GraphicsInterface
shouldGenerateCacheIfMissing?: boolean
apiClientFactory: ApiClientFactory
emitter?: GlobalEmitter
featureInstaller?: FeatureInstaller
}