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 */
1418import { existsSync , cpSync , writeFileSync , readFileSync } from 'fs'
1519import { fileURLToPath } from 'url'
@@ -31,35 +35,42 @@ Usage:
3135
3236Options:
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
3842Exit codes: 0 ok · 2 validation failed · 3 render failed · 64 usage error
3943Edit the YAML files in cv-content/ and re-run "cvx build".
4044Docs: 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
5161Next 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
5768async 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
97113let command = null
114+ let jsonMode = false
98115try {
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