Skip to content

Commit a920f4d

Browse files
authored
Merge pull request #38 from nanookclaw/fix/evaluate-json-format
feat(cli): add JSON format for evaluate
2 parents 6398541 + 64874be commit a920f4d

2 files changed

Lines changed: 143 additions & 5 deletions

File tree

packages/cli/src/cli.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,18 @@ async function main() {
3030

3131
async function handleEvaluate(args: string[]) {
3232
const cvPath = args[0];
33-
const jdFlagIndex = args.indexOf("--jd");
34-
const jdPath = jdFlagIndex !== -1 ? args[jdFlagIndex + 1] : undefined;
33+
const jdPath = readOptionValue(args, "--jd");
34+
const format = readOptionValue(args, "--format") ?? "text";
3535

36-
if (!cvPath) {
37-
console.error("Usage: cv-builder evaluate <cv-file> [--jd <jd-file>]");
36+
if (!cvPath || cvPath.startsWith("--")) {
37+
console.error(
38+
"Usage: cv-builder evaluate <cv-file> [--jd <jd-file>] [--format <text|json>]"
39+
);
40+
process.exit(1);
41+
}
42+
43+
if (format !== "text" && format !== "json") {
44+
console.error(`Unknown --format value: ${format}. Supported: text, json`);
3845
process.exit(1);
3946
}
4047

@@ -46,6 +53,11 @@ async function handleEvaluate(args: string[]) {
4653
jd: jdContent ? { content: jdContent } : undefined,
4754
});
4855

56+
if (format === "json") {
57+
console.log(JSON.stringify(result, null, 2));
58+
return;
59+
}
60+
4961
console.log(`\n CV Score: ${result.score}/5\n`);
5062
console.log(` Archetype detected: ${result.archetype.name}\n`);
5163
console.log(" Dimensions:");
@@ -74,6 +86,21 @@ async function handleEvaluate(args: string[]) {
7486
console.log(`\n ATS Compatible: ${result.atsCompatible ? "✓ Yes" : "✗ No"}\n`);
7587
}
7688

89+
function readOptionValue(args: string[], option: string): string | undefined {
90+
const index = args.indexOf(option);
91+
if (index === -1) {
92+
return undefined;
93+
}
94+
95+
const value = args[index + 1];
96+
if (!value || value.startsWith("--")) {
97+
console.error(`Missing value for ${option}`);
98+
process.exit(1);
99+
}
100+
101+
return value;
102+
}
103+
77104
function handleListArchetypes() {
78105
const archetypes = listArchetypes();
79106
console.log("\n Available role archetypes:\n");
@@ -92,13 +119,16 @@ function printHelp() {
92119
cv-builder <command> [options]
93120
94121
COMMANDS
95-
evaluate <cv-file> [--jd <jd-file>] Score your CV (optionally against a JD)
122+
evaluate <cv-file> [--jd <jd-file>] [--format <text|json>]
123+
Score your CV (optionally against a JD).
124+
Use --format json for machine-readable output.
96125
archetypes List available role archetypes
97126
help Show this help
98127
99128
EXAMPLES
100129
cv-builder evaluate ./my-cv.md
101130
cv-builder evaluate ./my-cv.md --jd ./job-description.md
131+
cv-builder evaluate ./my-cv.md --format json
102132
cv-builder archetypes
103133
104134
More commands coming soon: tailor, export, suggest

packages/cli/tests/cli.test.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { spawnSync } from "node:child_process";
2+
import { existsSync, mkdtempSync, writeFileSync } from "node:fs";
3+
import { tmpdir } from "node:os";
4+
import { dirname, resolve } from "node:path";
5+
import { fileURLToPath } from "node:url";
6+
import { beforeAll, describe, expect, it } from "vitest";
7+
8+
const __dirname = dirname(fileURLToPath(import.meta.url));
9+
const cliEntry = resolve(__dirname, "../dist/cli.js");
10+
11+
const CV = `# Jane Doe
12+
13+
## Experience
14+
15+
### Senior Engineer — Acme (2021 – 2024)
16+
17+
- Shipped a payments service in Python on PostgreSQL handling 2M requests/day
18+
- Reduced p99 latency from 800ms to 120ms by rewriting the hot path in Rust
19+
- Led migration from Redis to Kafka, cutting infrastructure cost by 35%
20+
`;
21+
22+
let cvPath: string;
23+
let jdPath: string;
24+
25+
beforeAll(() => {
26+
if (!existsSync(cliEntry)) {
27+
throw new Error(
28+
`CLI not built at ${cliEntry}. Run \`pnpm --filter @cv-builder/cli build\` first.`
29+
);
30+
}
31+
const dir = mkdtempSync(resolve(tmpdir(), "cv-builder-cli-test-"));
32+
cvPath = resolve(dir, "cv.md");
33+
jdPath = resolve(dir, "jd.md");
34+
writeFileSync(cvPath, CV, "utf-8");
35+
writeFileSync(
36+
jdPath,
37+
"Looking for a Python engineer with PostgreSQL and Kafka experience.",
38+
"utf-8"
39+
);
40+
});
41+
42+
function runCli(args: string[]) {
43+
return spawnSync("node", [cliEntry, ...args], { encoding: "utf-8" });
44+
}
45+
46+
describe("cv-builder evaluate --format json", () => {
47+
it("emits a valid EvaluationResult JSON object on stdout", () => {
48+
const result = runCli(["evaluate", cvPath, "--format", "json"]);
49+
50+
expect(result.status).toBe(0);
51+
const parsed = JSON.parse(result.stdout);
52+
expect(typeof parsed.score).toBe("number");
53+
expect(Array.isArray(parsed.dimensions)).toBe(true);
54+
expect(parsed.dimensions.length).toBeGreaterThan(0);
55+
expect(Array.isArray(parsed.strengths)).toBe(true);
56+
expect(Array.isArray(parsed.issues)).toBe(true);
57+
expect(Array.isArray(parsed.rewrites)).toBe(true);
58+
expect(parsed.archetype).toBeDefined();
59+
expect(typeof parsed.atsCompatible).toBe("boolean");
60+
});
61+
62+
it("works alongside --jd", () => {
63+
const result = runCli(["evaluate", cvPath, "--jd", jdPath, "--format", "json"]);
64+
65+
expect(result.status).toBe(0);
66+
const parsed = JSON.parse(result.stdout);
67+
expect(typeof parsed.score).toBe("number");
68+
expect(Array.isArray(parsed.dimensions)).toBe(true);
69+
expect(parsed.dimensions.length).toBeGreaterThan(0);
70+
expect(Array.isArray(parsed.strengths)).toBe(true);
71+
expect(Array.isArray(parsed.issues)).toBe(true);
72+
expect(Array.isArray(parsed.rewrites)).toBe(true);
73+
expect(parsed.archetype).toBeDefined();
74+
expect(typeof parsed.atsCompatible).toBe("boolean");
75+
});
76+
77+
it("preserves human-readable output by default", () => {
78+
const result = runCli(["evaluate", cvPath]);
79+
80+
expect(result.status).toBe(0);
81+
expect(result.stdout).toContain("CV Score:");
82+
expect(() => JSON.parse(result.stdout)).toThrow();
83+
});
84+
85+
it("rejects unknown --format values", () => {
86+
const result = runCli(["evaluate", cvPath, "--format", "yaml"]);
87+
88+
expect(result.status).not.toBe(0);
89+
expect(result.stderr).toMatch(/format/i);
90+
});
91+
92+
it("rejects missing option values", () => {
93+
const missingFormat = runCli(["evaluate", cvPath, "--format"]);
94+
const missingJd = runCli(["evaluate", cvPath, "--jd", "--format", "json"]);
95+
96+
expect(missingFormat.status).not.toBe(0);
97+
expect(missingFormat.stderr).toContain("Missing value for --format");
98+
expect(missingJd.status).not.toBe(0);
99+
expect(missingJd.stderr).toContain("Missing value for --jd");
100+
});
101+
102+
it("rejects an option token in place of the CV file", () => {
103+
const result = runCli(["evaluate", "--format", "json"]);
104+
105+
expect(result.status).not.toBe(0);
106+
expect(result.stderr).toContain("Usage: cv-builder evaluate <cv-file>");
107+
});
108+
});

0 commit comments

Comments
 (0)