alwaysai
Version:
The alwaysAI command-line interface (CLI)
259 lines (237 loc) • 6.94 kB
text/typescript
import * as logSymbols from 'log-symbols';
import * as chalk from 'chalk';
import { join } from 'path';
import { Choice } from 'prompts';
import { CliTerseError, CliUsageError } from '@alwaysai/alwayscli';
import { readdirSync } from 'fs-extra';
import * as tempy from 'tempy';
import { PLEASE_REPORT_THIS_ERROR_MESSAGE } from '../../constants';
import {
RequiredWithYesMessage,
promptForInput,
logger,
echo,
JsSpawner,
copyFiles,
fetchFilestream
} from '../../util';
import { confirmPromptComponent } from '../general';
import { getProjectByUUID } from '../../core/project';
import { CliAuthenticationClient, CliRpcClient } from '../../infrastructure';
import { stringifyError } from '../../util/stringify-error';
const BASE_URL = 'https://github.com/alwaysai/';
const ARCHIVE_PATH = 'archive/refs/heads/main.tar.gz';
const STARTER_APPS = [
{
webName: 'realtime_object_detector',
purpose: 'Object Detection',
downloadUrl: `${BASE_URL}object-detector/${ARCHIVE_PATH}`
},
{
webName: 'detector_tracker',
purpose: 'Object Detection with Tracking',
downloadUrl: `${BASE_URL}detector-tracker/${ARCHIVE_PATH}`
},
{
webName: 'image_classifier',
purpose: 'Image Classification',
downloadUrl: `${BASE_URL}image-classifier/${ARCHIVE_PATH}`
},
{
webName: 'semantic_segmentation_voc',
purpose: 'Semantic Segmentation',
downloadUrl: `${BASE_URL}semantic-segmentation-voc/${ARCHIVE_PATH}`
},
{
webName: 'realtime_pose_estimator',
purpose: 'Human Pose Estimation',
downloadUrl: `${BASE_URL}pose-estimator/${ARCHIVE_PATH}`
},
{
webName: 'hello_world_face_detector',
purpose: 'Face Detector',
downloadUrl: `${BASE_URL}face-detector/${ARCHIVE_PATH}`
},
{
webName: 'instance_segmentation',
purpose: 'Instance Segmentation',
downloadUrl: `${BASE_URL}instance-segmentation/${ARCHIVE_PATH}`
}
];
export async function projectSelectComponent(props: {
yes: boolean;
projectUuid?: string;
}) {
const { yes, projectUuid } = props;
if (yes && !projectUuid) {
throw new CliUsageError(RequiredWithYesMessage('project'));
}
const project = projectUuid
? await getProjectByUUID(projectUuid)
: await selectFromListOfProjects();
if (project.starter_app) {
let appName = project.starter_app;
// Replace legacy "hello_world" starter app with face detector on outdated projects
if (appName === 'hello_world') {
appName = 'hello_world_face_detector';
}
await downloadStarterApp({ yes, appName });
}
return project;
}
async function selectFromListOfProjects() {
const authInfo = await CliAuthenticationClient().getInfo();
const projects = await CliRpcClient().listProjectsUserIsCollaborator({
user_name: authInfo.username
});
const choices: Choice[] = [
{ title: chalk.green.bold('Create new project'), value: 'new' }
];
projects.forEach((project, index) => {
choices.push({
title: project.name,
value: `${index}`
});
});
const choice = await promptForInput({
purpose: 'to select a project',
questions: [
{
type: 'select',
name: 'project',
message: 'Select a project or create a new project',
initial: 0,
choices
}
]
});
if (choice.project === 'new') {
return await createProject();
}
return projects[choice.project];
}
async function createProject() {
const { username } = await CliAuthenticationClient().getInfo();
const team = await CliRpcClient().listTeamsUserIsOwner({
user_name: username
});
// TODO: Need to limit input characters to match dashboard implementation
const input = await promptForInput({
purpose: 'to choose a project name',
questions: [
{
type: 'text',
name: 'name',
message: 'Enter a project name:'
}
]
});
const name = input.name;
const choice = await promptForInput({
purpose: 'to select a project type',
questions: [
{
type: 'select',
name: 'type',
message: 'How would you like to initialize your project?',
initial: 0,
choices: [
{ title: 'From a Starter App', value: 'starterApp' },
{ title: 'As an empty app', value: 'empty' }
]
}
]
});
let starterApp = '';
switch (choice.type) {
case 'starterApp': {
const choices: Choice[] = [];
STARTER_APPS.forEach((app) => {
choices.push({
title: app.purpose,
value: app.webName
});
});
const choice = await promptForInput({
purpose: 'to select a starter app',
questions: [
{
type: 'select',
name: 'app',
message: 'Select a Starter App',
initial: 0,
choices
}
]
});
starterApp = choice.app;
break;
}
case 'empty':
default:
// Do nothing
}
const project = await CliRpcClient().createProject({
owner: username,
team_id: team[0].id,
name,
description: '',
git_url: '',
starter_app: starterApp
});
return project;
}
async function checkDirIsEmpty(dir) {
const fileNames = readdirSync(dir);
return fileNames.length === 0;
}
async function downloadStarterApp(props: { yes: boolean; appName: string }) {
const { yes, appName } = props;
let url: string | undefined = undefined;
if ((await checkDirIsEmpty(process.cwd())) === false) {
const proceed =
yes ||
(await confirmPromptComponent({
message:
'Would you like to download the Starter App? Files in this directory may be overwritten!'
}));
if (!proceed) {
echo('Skipping download of starter app!');
return;
}
}
for (const app of STARTER_APPS) {
if (app.webName === appName) {
url = app.downloadUrl;
break;
}
}
if (url === undefined) {
throw new CliTerseError(`Starter App "${appName}" not found!`);
}
const tmpDir = tempy.directory();
try {
const fileStream = await fetchFilestream(url);
const spawner = JsSpawner({ path: tmpDir });
await spawner.untar(fileStream);
} catch (e) {
logger.error(stringifyError(e));
throw new CliTerseError(
`Failed to download "${url}", check your internet connection and retry`
);
}
const fileNames = readdirSync(tmpDir);
if (fileNames.length !== 1 || !fileNames[0]) {
logger.error(
`Expected application to contain a single directory: ${fileNames}`
);
throw new CliTerseError(
`Expected application to contain a single directory. ${PLEASE_REPORT_THIS_ERROR_MESSAGE}`
);
}
const srcSpawner = JsSpawner({ path: join(tmpDir, fileNames[0]) });
const destSpawner = JsSpawner({ path: process.cwd() });
await copyFiles(srcSpawner, destSpawner);
await srcSpawner.rimraf();
echo(`${logSymbols.success} Download Starter App`);
}