Skip to content

Commit 97e8232

Browse files
committed
Sprint 1.3: agent-legible CLI — --json everywhere, stable envelopes
- --json on init/validate/build: stdout carries exactly one JSON object; logs and layout-fallback warnings go to stderr (renderCV warn is now routed explicitly) - error envelopes { ok: false, error: { code, message } } with structured codes: already-exists, render-failed, unknown-command - exit codes finalized and documented in --help: 0 ok / 2 validation / 3 render / 64 usage (init-refusal and unknown-command are usage errors, render failures no longer share exit 1 with everything else) - every command non-interactive; help states the agent contract
1 parent eb041e5 commit 97e8232

1 file changed

Lines changed: 35 additions & 14 deletions

File tree

bin/cvx.js

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
* Imports from ../lib (the published transform of src/pdf). In a repo checkout
1010
* run `npm run build:lib` first, or use the `npm run pdf` scripts instead.
1111
*
12-
* Exit codes: 0 success · 2 validation failed · 3 render failed · 64 usage error
12+
* Contract for agents and scripts:
13+
* - exit codes: 0 success · 2 validation failed · 3 render failed · 64 usage error
14+
* - with --json, stdout carries exactly one JSON object (the result);
15+
* logs and warnings go to stderr. Errors become { ok: false, error: {...} }.
16+
* - every command is non-interactive.
1317
*/
1418
import { existsSync, cpSync, writeFileSync, readFileSync } from 'fs'
1519
import { fileURLToPath } from 'url'
@@ -31,35 +35,42 @@ Usage:
3135
3236
Options:
3337
--strict validate: treat warnings (e.g. unknown keys) as errors
34-
--json Machine-readable result on stdout (validate)
38+
--json Machine-readable result on stdout; logs on stderr
3539
-h, --help Show this help
3640
-v, --version Show version
3741
3842
Exit codes: 0 ok · 2 validation failed · 3 render failed · 64 usage error
3943
Edit the YAML files in cv-content/ and re-run "cvx build".
4044
Docs: https://github.com/hrtips/cvx#readme`
4145

42-
async function init() {
46+
const emit = (obj) => console.log(JSON.stringify(obj, null, 2))
47+
48+
async function init({ json }) {
4349
const dest = join(process.cwd(), 'cv-content')
4450
if (existsSync(dest)) {
45-
console.error(`cv-content/ already exists here — refusing to overwrite.`)
51+
if (json) emit({ command: 'init', ok: false, error: { code: 'already-exists', message: 'cv-content/ already exists here — refusing to overwrite' } })
52+
else console.error(`cv-content/ already exists here — refusing to overwrite.`)
4653
process.exit(EXIT.usage)
4754
}
4855
cpSync(join(pkgRoot, 'template', 'cv-content'), dest, { recursive: true })
49-
console.log(`✅ Created cv-content/ with starter content.
56+
if (json) {
57+
emit({ command: 'init', ok: true, dest: 'cv-content' })
58+
} else {
59+
console.log(`✅ Created cv-content/ with starter content.
5060
5161
Next steps:
5262
1. Edit cv-content/*.yaml with your details
5363
2. Drop your photo at cv-content/images/profile.jpg
5464
3. Run: npx @hrtips/cvx build (or just "cvx build" if installed globally)`)
65+
}
5566
}
5667

5768
async function validate({ strict, json }) {
5869
const { validateContent } = await import('../lib/pdf/validateContent.js')
5970
const result = validateContent({ contentDir: join(process.cwd(), 'cv-content'), strict })
6071

6172
if (json) {
62-
console.log(JSON.stringify({ command: 'validate', schemaVersion: 1, strict, ...result }, null, 2))
73+
emit({ command: 'validate', ok: result.ok, schemaVersion: 1, strict, errors: result.errors, warnings: result.warnings, checked: result.checked })
6374
} else {
6475
const byFile = new Map()
6576
for (const [sev, list] of [['error', result.errors], ['warning', result.warnings]])
@@ -82,19 +93,25 @@ async function validate({ strict, json }) {
8293
process.exit(result.ok ? EXIT.ok : EXIT.validation)
8394
}
8495

85-
async function build(ats) {
96+
async function build({ ats, json }) {
8697
const { renderCV } = await import('../lib/pdf/render.js')
8798
const { buffer, filename, themeName, layoutName } = await renderCV({
8899
contentDir: join(process.cwd(), 'cv-content'),
89100
fontsDir: join(pkgRoot, 'lib', 'fonts'),
90101
ats,
102+
warn: (msg) => console.error(`⚠ ${msg}`),
91103
})
92104
writeFileSync(join(process.cwd(), filename), buffer)
93-
const mode = ats ? 'ATS' : `theme: ${themeName}, layout: ${layoutName}`
94-
console.log(`✅ ${filename} (${(buffer.byteLength / 1024).toFixed(0)} KB, ${mode})`)
105+
if (json) {
106+
emit({ command: 'build', ok: true, filename, bytes: buffer.byteLength, ats, theme: ats ? null : themeName, layout: ats ? null : layoutName })
107+
} else {
108+
const mode = ats ? 'ATS' : `theme: ${themeName}, layout: ${layoutName}`
109+
console.log(`✅ ${filename} (${(buffer.byteLength / 1024).toFixed(0)} KB, ${mode})`)
110+
}
95111
}
96112

97113
let command = null
114+
let jsonMode = false
98115
try {
99116
const { values, positionals } = parseArgs({
100117
options: {
@@ -107,22 +124,26 @@ try {
107124
allowPositionals: true,
108125
})
109126
command = positionals[0] ?? null
127+
jsonMode = values.json
110128

111129
if (values.version) {
112130
console.log(version)
113131
} else if (values.help || positionals.length === 0) {
114132
console.log(HELP)
115133
} else if (command === 'init') {
116-
await init()
134+
await init(values)
117135
} else if (command === 'validate') {
118136
await validate(values)
119137
} else if (command === 'build') {
120-
await build(values.ats)
138+
await build(values)
121139
} else {
122-
console.error(`Unknown command: ${command}\n\n${HELP}`)
140+
if (jsonMode) emit({ command, ok: false, error: { code: 'unknown-command', message: `unknown command: ${command}` } })
141+
else console.error(`Unknown command: ${command}\n\n${HELP}`)
123142
process.exit(EXIT.usage)
124143
}
125144
} catch (err) {
126-
console.error(err.message)
127-
process.exit(command === 'build' ? EXIT.render : EXIT.usage)
145+
const code = command === 'build' ? EXIT.render : EXIT.usage
146+
if (jsonMode) emit({ command, ok: false, error: { code: command === 'build' ? 'render-failed' : 'usage', message: err.message } })
147+
else console.error(err.message)
148+
process.exit(code)
128149
}

0 commit comments

Comments
 (0)