-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcli.ts
More file actions
101 lines (89 loc) · 3.19 KB
/
cli.ts
File metadata and controls
101 lines (89 loc) · 3.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import { Command } from "commander";
import { EXIT_CODE } from "./types.js";
import { CerberusError } from "./errors.js";
const program = new Command();
program
.name("cerberus")
.description("Statistical CI/CD for AI agents")
.version("0.1.0");
program
.command("run")
.description("Run the test suite defined in cerberus.yaml")
.option("-c, --config <path>", "Path to config file", "cerberus.yaml")
.option("--json", "Output results as JSON")
.option("-o, --output <path>", "Write JSON results to file")
.action(async (options: { config: string; json?: boolean; output?: string }) => {
// Phase 2 will wire this up
const { loadConfig } = await import("./config.js");
try {
const config = await loadConfig(options.config);
const { runSuite } = await import("./runner.js");
const { formatResults, writeJsonOutput, persistResult } = await import("./output.js");
const result = await runSuite(config, {
json: options.json ?? false,
});
if (options.json || options.output) {
const json = writeJsonOutput(result);
if (options.output) {
const { writeFile } = await import("node:fs/promises");
await writeFile(options.output, json + "\n", "utf-8");
}
if (options.json) {
process.stdout.write(json + "\n");
}
} else {
formatResults(result);
}
// Persist result to .cerberus/runs/
await persistResult(result);
process.exit(result.status === "pass" ? EXIT_CODE.PASS
: result.status === "fail" ? EXIT_CODE.FAIL
: result.status === "inconclusive" ? EXIT_CODE.INCONCLUSIVE
: EXIT_CODE.RUNTIME_ERROR);
} catch (e) {
if (e instanceof CerberusError) {
process.stderr.write(`Error: ${e.message}\n`);
process.exit(e.exitCode);
}
throw e;
}
});
program
.command("init")
.description("Scaffold a cerberus.yaml config file")
.option("--force", "Overwrite existing config")
.action(async (options: { force?: boolean }) => {
// Phase 4 will implement this
const { existsSync } = await import("node:fs");
const { writeFile, mkdir } = await import("node:fs/promises");
const configPath = "cerberus.yaml";
if (!options.force && existsSync(configPath)) {
process.stderr.write(`Error: ${configPath} already exists. Use --force to overwrite.\n`);
process.exit(EXIT_CODE.CONFIG_ERROR);
}
const template = `# Cerberus configuration
adapter:
command: "node ./agent.js --scenario {{scenario}}"
timeout: 30000
studies:
- name: default
scenario: scenarios/example.yaml
contracts:
- name: produces-valid-output
type: code
assert: "output.meta.exitCode === 0"
threshold: 0.95
trials: 20
`;
const scenarioTemplate = `input: |
Your test input goes here.
metadata:
category: default
`;
await mkdir("scenarios", { recursive: true });
await writeFile(configPath, template, "utf-8");
await writeFile("scenarios/example.yaml", scenarioTemplate, "utf-8");
process.stdout.write(`Created ${configPath} and scenarios/example.yaml\n`);
process.stdout.write("Edit the config and run: cerberus run\n");
});
program.parse();