UNPKG

alwaysai

Version:

The alwaysAI command-line interface (CLI)

322 lines (301 loc) 8.36 kB
import { existsSync } from 'fs'; import { CliUsageError, CliTerseError } from '@alwaysai/alwayscli'; import { buildDockerImageComponent } from '../docker'; import { checkUserIsLoggedInComponent } from '../user'; import { appCheckComponent } from './app-check-component'; import { appCleanComponent } from './app-clean-component'; import { appConfigureComponent } from './app-configure-component'; import { createTargetDirectoryComponent } from './target'; import { appInstallModelsComponent } from './models/app-install-models-component'; import { ALWAYSAI_CLI_EXECUTABLE_NAME, DOCKER_TEST_IMAGE_ID, PLEASE_REPORT_THIS_ERROR_MESSAGE } from '../../constants'; import { JsSpawner, SshSpawner, runWithSpinner, logger, Spawner, Spinner, SshDockerSpawner, copyFiles, stringifyError } from '../../util'; import { TargetJsonFile, getTargetHardwareUuid, TargetConfig, TargetJsonFileReturnType, getPythonVenvPaths, createPythonVenv, installPythonReqs } from '../../core/app'; import { getDeviceByUuid } from '../../infrastructure'; import { connectBySshComponent, findOrWritePrivateKeyFileComponent } from '../general'; import { PYTHON_REQUIREMENTS_FILE_NAME } from '../../paths'; export const APP_IGNORE_FILES = ['models', 'node_modules', '.git', 'venv']; export async function appInstallComponent(props: { yes: boolean; clean: boolean; pull: boolean; source: boolean; models: boolean; docker: boolean; venv: boolean; excludes?: string[]; }) { const { yes, clean, pull, source, models, docker, venv, excludes } = props; const steps: string[] = []; // When any flag is set, only add steps determined by flags if ([source, models, docker, venv].some((element) => element === true)) { if (source) { steps.push('source'); } if (models) { steps.push('models'); } if (docker) { steps.push('docker'); } if (venv) { steps.push('venv'); } } else { // Otherwise, add all steps steps.push(...['source', 'models', 'docker', 'venv']); } await checkUserIsLoggedInComponent({ yes }); try { await appCheckComponent(); } catch (err) { if (yes) { throw new CliUsageError( `App is not properly configured. Did you run \`${ALWAYSAI_CLI_EXECUTABLE_NAME} app configure\`?` ); } else { await appConfigureComponent({ yes }); } } const targetJsonFile = TargetJsonFile(); const targetHostSpawner = targetJsonFile.readHostSpawner(); const targetCfg = targetJsonFile.read(); const sourceSpawner = JsSpawner(); switch (targetCfg.targetProtocol) { case 'native:': case 'docker:': { if (clean) { await appCleanComponent({ yes }); } break; } case 'ssh+docker:': { const { targetHostname, targetPath } = targetCfg; await findOrWritePrivateKeyFileComponent({ yes }); await connectBySshComponent({ targetHostname }); if (clean) { await appCleanComponent({ yes }); } await createTargetDirectoryComponent({ targetHostname, targetPath }); const projectDevice = targetCfg.deviceId; let getDevice; try { getDevice = await getDeviceByUuid({ uuid: projectDevice }); } catch (error) { logger.error(stringifyError(error)); throw new CliTerseError( 'Device does not exist in the selected project.' ); } const hardwareId = await getTargetHardwareUuid( SshSpawner({ targetHostname }) ); if (hardwareId !== getDevice.hardware_ids) { throw new CliTerseError( `Target device does not match the one selected. Please run ${ALWAYSAI_CLI_EXECUTABLE_NAME} app configure again.` ); } break; } default: } if (steps.includes('source')) { await installSource({ targetCfg, sourceSpawner, targetHostSpawner, excludes }); } if (steps.includes('docker')) { await buildDocker({ targetCfg, targetJsonFile, targetHostSpawner, pull }); } if (steps.includes('models')) { await installModels({ targetJsonFile }); } if (steps.includes('venv')) { await installVenv({ targetCfg, sourceSpawner, targetJsonFile }); } } async function installSource(props: { targetCfg: TargetConfig; sourceSpawner: Spawner; targetHostSpawner: Spawner; excludes?: string[]; }) { const { targetCfg, sourceSpawner, targetHostSpawner } = props; switch (targetCfg.targetProtocol) { case 'ssh+docker:': { const { targetHostname, targetPath } = targetCfg; const busyboxSpawner = SshDockerSpawner({ targetHostname, targetPath, dockerImageId: DOCKER_TEST_IMAGE_ID }); const excludes = props.excludes ? props.excludes.concat(APP_IGNORE_FILES) : APP_IGNORE_FILES; await runWithSpinner( async () => { await copyFiles(sourceSpawner, busyboxSpawner, excludes); logger.debug( await targetHostSpawner.run({ exe: 'docker', args: [ 'run', '--rm', '--workdir', '/app', '--volume', '$(pwd):/app', DOCKER_TEST_IMAGE_ID, 'chown', '-R', '$(id -u ${USER}):$(id -g ${USER})', '/app' ], cwd: '.' }) ); }, [], 'Copy application to target' ); break; } default: } } export async function buildDocker(props: { targetCfg: TargetConfig; targetJsonFile: TargetJsonFileReturnType; targetHostSpawner: Spawner; pull: boolean; }) { const { targetCfg, targetJsonFile, targetHostSpawner, pull } = props; switch (targetCfg.targetProtocol) { case 'docker:': case 'ssh+docker:': { const targetHardware = targetCfg.targetHardware; const dockerImageId = await buildDockerImageComponent({ targetHostSpawner, targetHardware, pullBaseImage: pull }); targetCfg.dockerImageId = dockerImageId; targetJsonFile.update((targetCfg) => { switch (targetCfg.targetProtocol) { case 'docker:': case 'ssh+docker:': { targetCfg.dockerImageId = dockerImageId; break; } case 'native:': default: { throw new CliTerseError( `Invalid target protocol (${targetCfg.targetProtocol})! ${PLEASE_REPORT_THIS_ERROR_MESSAGE}` ); } } }); } } } async function installModels(props: { targetJsonFile: TargetJsonFileReturnType; }) { const { targetJsonFile } = props; const targetSpawner = targetJsonFile.readHostSpawner(); await appInstallModelsComponent(targetSpawner); } export async function installVenv(props: { targetCfg: TargetConfig; sourceSpawner: Spawner; targetJsonFile: TargetJsonFileReturnType; }) { const { targetCfg, sourceSpawner, targetJsonFile } = props; const pythonVenvPaths = await getPythonVenvPaths({ targetCfg }); let targetSpawner: Spawner; switch (targetCfg.targetProtocol) { case 'native:': { targetSpawner = sourceSpawner; break; } case 'docker:': case 'ssh+docker:': { targetSpawner = targetJsonFile.readContainerSpawner({ ignoreTargetHardware: true }); break; } default: throw new CliTerseError( `Invalid target protocol(${targetCfg})! ${PLEASE_REPORT_THIS_ERROR_MESSAGE}` ); } const spinner = Spinner('Create python virtual environment'); try { const installed = await createPythonVenv({ targetSpawner, pythonVenvPaths, logger }); if (installed === false) { spinner.succeed('Found python virtual environment'); } else { spinner.succeed(); } } catch (exception) { spinner.fail(); throw exception; } if (existsSync(PYTHON_REQUIREMENTS_FILE_NAME)) { await runWithSpinner( installPythonReqs, [ { reqFilePath: PYTHON_REQUIREMENTS_FILE_NAME, targetSpawner, pythonVenvPaths, logger } ], 'Install python dependencies' ); } }