|
| 1 | +import { Arguments, Argv, CommandModule } from 'yargs'; |
| 2 | +import chalk from 'chalk'; |
| 3 | +import process from 'process'; |
| 4 | +import { getEnvironmentByPath } from './configuration/environment-resolution.js'; |
| 5 | +import { |
| 6 | + BUILT_IN_ENVIRONMENTS, |
| 7 | + LLM_OUTPUT_DIR, |
| 8 | +} from './configuration/constants.js'; |
| 9 | +import { UserFacingError } from './utils/errors.js'; |
| 10 | +import { existsSync, rmSync } from 'fs'; |
| 11 | +import { readFile, readdir } from 'fs/promises'; |
| 12 | +import { join } from 'path'; |
| 13 | +import { glob } from 'tinyglobby'; |
| 14 | +import { LlmResponseFile } from './shared-interfaces.js'; |
| 15 | +import { |
| 16 | + setupProjectStructure, |
| 17 | + writeResponseFiles, |
| 18 | +} from './orchestration/file-system.js'; |
| 19 | +import { serveApp } from './builder/serve-app.js'; |
| 20 | +import { ProgressLogger, ProgressType } from './progress/progress-logger.js'; |
| 21 | +import { formatTitleCard } from './reporting/format.js'; |
| 22 | + |
| 23 | +export const RunModule = { |
| 24 | + builder, |
| 25 | + handler, |
| 26 | + command: 'run', |
| 27 | + describe: 'Run an evaluated app locally', |
| 28 | +} satisfies CommandModule<{}, Options>; |
| 29 | + |
| 30 | +interface Options { |
| 31 | + environment: string; |
| 32 | + prompt: string; |
| 33 | +} |
| 34 | + |
| 35 | +function builder(argv: Argv): Argv<Options> { |
| 36 | + return argv |
| 37 | + .option('environment', { |
| 38 | + type: 'string', |
| 39 | + alias: ['env'], |
| 40 | + default: '', |
| 41 | + description: 'Path to the environment configuration file', |
| 42 | + }) |
| 43 | + .option('prompt', { |
| 44 | + type: 'string', |
| 45 | + default: '', |
| 46 | + description: 'ID of the prompt within the environment that should be run', |
| 47 | + }) |
| 48 | + .version(false) |
| 49 | + .help(); |
| 50 | +} |
| 51 | + |
| 52 | +async function handler(options: Arguments<Options>): Promise<void> { |
| 53 | + try { |
| 54 | + await runApp(options); |
| 55 | + } catch (error) { |
| 56 | + if (error instanceof UserFacingError) { |
| 57 | + console.error(chalk.red(error.message)); |
| 58 | + } else { |
| 59 | + throw error; |
| 60 | + } |
| 61 | + } |
| 62 | +} |
| 63 | + |
| 64 | +async function runApp(options: Options) { |
| 65 | + const { environment, rootPromptDef, files } = await resolveConfig(options); |
| 66 | + const progress = new ErrorOnlyProgressLogger(); |
| 67 | + |
| 68 | + console.log( |
| 69 | + `Setting up the "${environment.displayName}" environment with the "${rootPromptDef.name}" prompt...` |
| 70 | + ); |
| 71 | + |
| 72 | + const { directory, cleanup } = await setupProjectStructure( |
| 73 | + environment, |
| 74 | + rootPromptDef, |
| 75 | + progress |
| 76 | + ); |
| 77 | + |
| 78 | + const processExitPromise = new Promise<void>((resolve) => { |
| 79 | + const done = () => { |
| 80 | + () => { |
| 81 | + try { |
| 82 | + // Note: we don't use `cleanup` here, because the call needs to be synchronous. |
| 83 | + rmSync(directory, { recursive: true }); |
| 84 | + } catch {} |
| 85 | + resolve(); |
| 86 | + }; |
| 87 | + }; |
| 88 | + |
| 89 | + process.on('exit', done); |
| 90 | + process.on('close', done); |
| 91 | + process.on('SIGINT', done); |
| 92 | + }); |
| 93 | + |
| 94 | + try { |
| 95 | + await writeResponseFiles(directory, files, environment, rootPromptDef.name); |
| 96 | + |
| 97 | + await serveApp( |
| 98 | + environment.serveCommand, |
| 99 | + rootPromptDef.name, |
| 100 | + directory, |
| 101 | + () => {}, |
| 102 | + async (url) => { |
| 103 | + console.log(); |
| 104 | + console.log(formatTitleCard(`🎉 App is up and running at ${url}`)); |
| 105 | + await processExitPromise; |
| 106 | + } |
| 107 | + ); |
| 108 | + } finally { |
| 109 | + await cleanup(); |
| 110 | + } |
| 111 | +} |
| 112 | + |
| 113 | +async function resolveConfig(options: Options) { |
| 114 | + if (!options.environment) { |
| 115 | + throw new UserFacingError( |
| 116 | + [ |
| 117 | + '`--env` flag has not been specified. You have the following options:', |
| 118 | + ' - Pass a path to an environment config file using the `--env` flag.', |
| 119 | + ' - Pass `--env=angular-example` or `--env=solid-example` to use one of our built-in example environments.', |
| 120 | + ' - Pass `--help` to see all available options.', |
| 121 | + ].join('\n') |
| 122 | + ); |
| 123 | + } else if (!options.prompt) { |
| 124 | + throw new UserFacingError( |
| 125 | + '`--prompt` flag has not been specified. ' + |
| 126 | + 'You have to pass a prompt name through the `--prompt` flag.' |
| 127 | + ); |
| 128 | + } |
| 129 | + |
| 130 | + const environment = await getEnvironmentByPath( |
| 131 | + BUILT_IN_ENVIRONMENTS.get(options.environment) || options.environment |
| 132 | + ); |
| 133 | + const environmentDir = join(LLM_OUTPUT_DIR, environment.id); |
| 134 | + |
| 135 | + if (!existsSync(environmentDir)) { |
| 136 | + throw new UserFacingError( |
| 137 | + `Could not find any LLM output for environment "${environment.displayName}" under "${environmentDir}"` |
| 138 | + ); |
| 139 | + } |
| 140 | + |
| 141 | + const prompts = await getPossiblePrompts(environmentDir); |
| 142 | + |
| 143 | + if (!prompts.includes(options.prompt)) { |
| 144 | + throw new UserFacingError( |
| 145 | + `There is no local LLM output for environment "${options.prompt}".\n` + |
| 146 | + `The following prompts have local data:\n` + |
| 147 | + prompts.map((p) => ` - ${p}`).join('\n') |
| 148 | + ); |
| 149 | + } |
| 150 | + |
| 151 | + const rootPromptDef = environment.executablePrompts.find( |
| 152 | + (p) => p.name === options.prompt |
| 153 | + ); |
| 154 | + |
| 155 | + if (!rootPromptDef) { |
| 156 | + throw new UserFacingError( |
| 157 | + `Environment "${environment.displayName}" does not have a prompt with a name of "${options.prompt}".\n` + |
| 158 | + `The following prompts are available:\n` + |
| 159 | + environment.executablePrompts.map((p) => ` - ${p.name}`).join('\n') |
| 160 | + ); |
| 161 | + } |
| 162 | + |
| 163 | + const promptDir = join(environmentDir, options.prompt); |
| 164 | + const filePaths = await glob('**/*', { cwd: promptDir }); |
| 165 | + const files: LlmResponseFile[] = await Promise.all( |
| 166 | + filePaths.map(async (path) => { |
| 167 | + return { |
| 168 | + filePath: path, |
| 169 | + code: await readFile(join(promptDir, path), 'utf8'), |
| 170 | + }; |
| 171 | + }) |
| 172 | + ); |
| 173 | + |
| 174 | + return { environment, rootPromptDef, files }; |
| 175 | +} |
| 176 | + |
| 177 | +async function getPossiblePrompts(environmentDir: string): Promise<string[]> { |
| 178 | + const entities = await readdir(environmentDir, { withFileTypes: true }); |
| 179 | + return entities |
| 180 | + .filter((entity) => entity.isDirectory()) |
| 181 | + .map((entity) => entity.name); |
| 182 | +} |
| 183 | + |
| 184 | +class ErrorOnlyProgressLogger implements ProgressLogger { |
| 185 | + initialize(): void {} |
| 186 | + finalize(): void {} |
| 187 | + |
| 188 | + log(_: unknown, type: ProgressType, message: string, details?: string) { |
| 189 | + if (type === 'error') { |
| 190 | + console.error(chalk.red(message)); |
| 191 | + |
| 192 | + if (details) { |
| 193 | + console.error(chalk.red(message)); |
| 194 | + } |
| 195 | + } |
| 196 | + } |
| 197 | +} |
0 commit comments