alwaysai
Version:
The alwaysAI command-line interface (CLI)
322 lines (301 loc) • 8.36 kB
text/typescript
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'
);
}
}