@sprucelabs/spruce-cli
Version:
Command line interface for building Spruce skills.
252 lines (214 loc) • 7.66 kB
text/typescript
import { execSync } from 'child_process'
import { diskUtil } from '@sprucelabs/spruce-skill-utils'
import TerminalInterface from '../interfaces/TerminalInterface'
import ImportService from '../services/ImportService'
import ServiceFactory from '../services/ServiceFactory'
import testUtil from '../tests/utilities/test.utility'
import { GraphicsTextEffect } from '../types/graphicsInterface.types'
import durationUtil from '../utilities/duration.utility'
import FeatureFixture from './fixtures/FeatureFixture'
import MercuryFixture from './fixtures/MercuryFixture'
require('dotenv').config()
const packageJsonContents = diskUtil.readFile(
diskUtil.resolvePath(__dirname, '..', '..', 'package.json')
)
const packageJson = JSON.parse(packageJsonContents)
const { testSkillCache } = packageJson
const testKeys = Object.keys(testSkillCache)
let remaining = testKeys.length
const term = new TerminalInterface(__dirname, true)
const start = new Date().getTime()
const testSkillsToCache =
process.env.TEST_SKILLS_TO_CACHE === '*'
? undefined
: process.env.TEST_SKILLS_TO_CACHE
const onlyInstall = testSkillsToCache?.split(',').map((t) => t.trim()) as
| string[]
| undefined
const shouldRunSequentially = !!process.argv.find(
(a) =>
a === '--shouldRunSequentially=true' || a === '--shouldRunSequentially'
)
const maxSimultaneous = parseInt(
process.env.MAX_SIMULTANEOUS_SKILL_CACHERS ?? '10',
10
)
let totalSimultaneous = 0
let progressInterval: any
const doesSupportColor = TerminalInterface.doesSupportColor()
let didMessagesChange = true
async function run() {
term.clear()
if (process.env.WILL_BUILD_CACHE_SCRIPT) {
term.renderLine('Running will-build cache script')
if (process.env.CLEAN_CACHE_SCRIPT) {
try {
execSync(process.env.CLEAN_CACHE_SCRIPT)
} catch {
/* Empty */
}
}
execSync(process.env.WILL_BUILD_CACHE_SCRIPT)
}
term.renderHeadline(`Found ${testKeys.length} skills to cache.`)
let messages: [string, any][] = []
const render = async () => {
if (didMessagesChange) {
term.moveCursorTo(0, 6)
for (const message of messages) {
term.eraseLine()
term.renderLine(message[0], message[1])
}
await term.stopLoading()
term.eraseLine()
term.renderLine('')
}
didMessagesChange = false
await term.startLoading(
`${`Building ${remaining} skill${dropInS(
remaining
)}`}. ${durationUtil.msToFriendly(getTimeSpent())}`
)
term.clearBelowCursor()
}
progressInterval = doesSupportColor && setInterval(render, 1000)
function getTimeSpent() {
const now = new Date().getTime()
const delta = now - start
return delta
}
function renderLine(lineNum: number, message: any, effects?: any) {
if (doesSupportColor) {
didMessagesChange = true
messages[lineNum] = [message, effects]
void render()
} else {
console.log(message)
}
}
function renderWarning(lineNum: number, message: any, effects?: any) {
if (doesSupportColor) {
didMessagesChange = true
messages[lineNum] = [message, effects]
void render()
} else {
console.log(message)
}
}
if (doesSupportColor) {
await term.startLoading(
`Building ${remaining} remaining skill${dropInS(remaining)}...`
)
}
if (shouldRunSequentially) {
await Promise.all(
testKeys.map((cacheKey, idx) => cacheOrSkip(idx, cacheKey))
)
} else {
const promises = testKeys.map(async (cacheKey, idx) => {
while (totalSimultaneous >= maxSimultaneous) {
await new Promise((resolve) => setTimeout(resolve, 1000))
}
totalSimultaneous++
await cacheOrSkip(idx, cacheKey)
totalSimultaneous--
})
await Promise.all(promises)
}
await term.stopLoading()
term.renderLine(`Done! ${durationUtil.msToFriendly(getTimeSpent())}`)
async function cacheOrSkip(lineNum: number, cacheKey: string) {
const { cacheTracker, cwd, fixture, options } = setup(cacheKey)
if (onlyInstall && onlyInstall.indexOf(cacheKey) === -1) {
renderLine(lineNum, `Skipping '${cacheKey}'.`, [
GraphicsTextEffect.Yellow,
])
remaining--
} else if (
cacheTracker[cacheKey] &&
diskUtil.doesDirExist(diskUtil.resolvePath(cwd, 'node_modules'))
) {
remaining--
renderLine(lineNum, `'${cacheKey}' already cached. Skipping...`, [
GraphicsTextEffect.Italic,
])
} else {
await cache(lineNum, cwd, cacheKey, fixture, options)
remaining--
}
}
function setup(cacheKey: string) {
const options = testSkillCache[cacheKey]
const importCacheDir = testUtil.resolveTestDir(
'spruce-cli-import-cache'
)
ImportService.setCacheDir(importCacheDir)
const serviceFactory = new ServiceFactory()
const cwd = testUtil.resolveTestDir(cacheKey)
const mercuryFixture = new MercuryFixture(cwd, serviceFactory)
const fixture = new FeatureFixture({
cwd,
serviceFactory,
ui: new TerminalInterface(cwd),
shouldGenerateCacheIfMissing: true,
apiClientFactory: mercuryFixture.getApiClientFactory(),
})
const cacheTracker = fixture.loadCacheTracker()
return { cacheTracker, cwd, fixture, options }
}
async function cache(
lineNum: number,
cwd: string,
cacheKey: string,
fixture: FeatureFixture,
options: any
) {
if (diskUtil.doesDirExist(cwd)) {
renderWarning(
lineNum,
`Found cached '${cacheKey}', but deleted it since it was not in the cache tracker...`,
[GraphicsTextEffect.Italic]
)
diskUtil.deleteDir(cwd)
}
renderLine(lineNum, `Starting to build '${cacheKey}'...`, [
GraphicsTextEffect.Green,
])
try {
await fixture.installFeatures(options, cacheKey)
renderLine(
lineNum,
`Done caching '${cacheKey}'. ${
remaining - 1
} remaining (${durationUtil.msToFriendly(getTimeSpent())})...`,
[GraphicsTextEffect.Green, GraphicsTextEffect.Bold]
)
} catch (err: any) {
renderLine(lineNum, `Error caching '${cacheKey}'...`, [
GraphicsTextEffect.Red,
GraphicsTextEffect.Bold,
])
renderLine(lineNum, `Error caching ${cacheKey}:\n\n${err.stack}`)
}
}
}
function dropInS(remaining: number) {
return remaining === 1 ? '' : 's'
}
void run()
.then(() => {
if (process.env.DID_BUILD_CACHE_SCRIPT) {
term.renderLine('Running did-build cache script')
execSync(process.env.DID_BUILD_CACHE_SCRIPT)
}
if (progressInterval) {
clearInterval(progressInterval)
}
})
.catch((err) => {
term.renderError(err)
if (progressInterval) {
clearInterval(progressInterval)
}
})