| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance | |
| * with the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, | |
| * software distributed under the License is distributed on an | |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| * KIND, either express or implied. See the License for the | |
| * specific language governing permissions and limitations | |
| * under the License. | |
| */ | |
| import { readFile } from 'node:fs/promises'; | |
| import { resolve } from 'node:path'; | |
| import { createExternalSubjectAdapter } from './external-subject.js'; | |
| import { | |
| createHarborExecutor, | |
| createPierExecutor, | |
| type HarnessExecutor, | |
| } from './harness-executor.js'; | |
| import { openExperimentDirectory } from './experiment-directory.js'; | |
| import type { ExperimentSpec } from './experiment.js'; | |
| import { createMakaSubjectAdapter } from './maka-subject.js'; | |
| import { runExperiment, type ExperimentExecutor, type SubjectAdapter } from './runner.js'; | |
| import { parseExperimentSpec } from './spec.js'; | |
| const USAGE = 'usage: maka eval run <spec.json> --out <directory> [--cell <cell-id>]'; | |
| export interface EvalCliDependencies { | |
| readonly loadExecutor: (spec: ExperimentSpec, specPath: string) => ExperimentExecutor; | |
| readonly subjects: readonly SubjectAdapter[]; | |
| readonly signal?: AbortSignal; | |
| readonly stdout: (text: string) => void; | |
| readonly stderr: (text: string) => void; | |
| } | |
| export async function runMakaEvalCli( | |
| argv: readonly string[], | |
| overrides: Partial<EvalCliDependencies> = {}, | |
| ): Promise<number> { | |
| const controller = overrides.signal ? undefined : new AbortController(); | |
| const signal = overrides.signal ?? controller?.signal; | |
| let interrupted: number | undefined; | |
| const interrupt = () => { | |
| interrupted = 130; | |
| controller?.abort(); | |
| }; | |
| const terminate = () => { | |
| interrupted = 143; | |
| controller?.abort(); | |
| }; | |
| if (controller) { | |
| process.once('SIGINT', interrupt); | |
| process.once('SIGTERM', terminate); | |
| } | |
| try { | |
| const command = parseArgs(argv); | |
| if (command.kind === 'help') { | |
| (overrides.stdout ?? process.stdout.write.bind(process.stdout))(`${USAGE}\n`); | |
| return 0; | |
| } | |
| const specPath = resolve(command.specPath); | |
| const spec = parseExperimentSpec(JSON.parse(await readFile(specPath, 'utf8')) as unknown); | |
| const directory = await openExperimentDirectory(resolve(command.outDir), spec); | |
| let executor: ExperimentExecutor; | |
| if (overrides.loadExecutor) { | |
| executor = overrides.loadExecutor(spec, specPath); | |
| } else { | |
| const builtin = builtinExecutor(spec, specPath); | |
| await builtin.preflight({ | |
| subjectCredentialNames: [ | |
| ...new Set(spec.subjects.flatMap((subject) => subject.credentials)), | |
| ], | |
| ...(signal ? { signal } : {}), | |
| }); | |
| executor = builtin; | |
| } | |
| const results = await runExperiment({ | |
| spec, | |
| store: directory.attempts, | |
| executor, | |
| subjects: overrides.subjects ?? [createMakaSubjectAdapter(), createExternalSubjectAdapter()], | |
| ...(command.cellIds.length > 0 ? { cellIds: command.cellIds } : {}), | |
| ...(signal ? { signal } : {}), | |
| }); | |
| const incomplete = spec.tasks.length * spec.repetitions * spec.subjects.length - results.size; | |
| (overrides.stdout ?? process.stdout.write.bind(process.stdout))( | |
| `${JSON.stringify({ experimentId: spec.id, cells: results.size, incomplete })}\n`, | |
| ); | |
| return interrupted ?? (incomplete === 0 ? 0 : 1); | |
| } catch (error) { | |
| (overrides.stderr ?? process.stderr.write.bind(process.stderr))( | |
| `maka eval: ${error instanceof Error ? error.message : String(error)}\n${USAGE}\n`, | |
| ); | |
| return interrupted ?? 2; | |
| } finally { | |
| if (controller) { | |
| process.removeListener('SIGINT', interrupt); | |
| process.removeListener('SIGTERM', terminate); | |
| } | |
| } | |
| } | |
| function builtinExecutor(spec: ExperimentSpec, specPath: string): HarnessExecutor { | |
| if (spec.executor.kind === 'harbor') return createHarborExecutor(spec.executor.config, specPath); | |
| if (spec.executor.kind === 'pier') return createPierExecutor(spec.executor.config, specPath); | |
| throw new Error(`unsupported executor: ${spec.executor.kind}`); | |
| } | |
| function parseArgs(argv: readonly string[]): | |
| | { readonly kind: 'help' } | |
| | { | |
| readonly kind: 'run'; | |
| readonly specPath: string; | |
| readonly outDir: string; | |
| readonly cellIds: readonly string[]; | |
| } { | |
| if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) return { kind: 'help' }; | |
| if (argv[0] !== 'run' || !argv[1] || argv[1].startsWith('-')) throw new Error(USAGE); | |
| let outDir: string | undefined; | |
| const cellIds: string[] = []; | |
| for (let index = 2; index < argv.length; index += 2) { | |
| const flag = argv[index]; | |
| const value = argv[index + 1]; | |
| if (!value || value.startsWith('-')) throw new Error(`${flag} requires a value`); | |
| if (flag === '--out') outDir = value; | |
| else if (flag === '--cell') cellIds.push(value); | |
| else throw new Error(`unexpected argument: ${flag}`); | |
| } | |
| if (!outDir) throw new Error('--out is required'); | |
| return { kind: 'run', specPath: argv[1], outDir, cellIds }; | |
| } |