UNPKG

alwaysai

Version:

The alwaysAI command-line interface (CLI)

259 lines (237 loc) 6.94 kB
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`); }