xtask/codex_schema/
regen.rs1use std::fs;
7use std::path::{Path, PathBuf};
8use std::process::Command;
9
10use anyhow::{bail, Context, Result};
11
12use super::{
13 load_combined_defs, merge, parse_gen_dir, CODEX_VERSION_PATH, METHODS_JSON_PATH,
14 PROTOCOL_SCHEMA_PATH,
15};
16
17pub fn run(args: &[String]) -> Result<()> {
18 let gen_dir = parse_args(args)?;
19 regen(&gen_dir)
20}
21
22fn parse_args(args: &[String]) -> Result<PathBuf> {
23 parse_gen_dir(
24 args,
25 "Usage: cargo xtask codex-schema regen <path-to-codex-generate-json-schema-output-dir>\n\
26 Generate that directory first with:\n \
27 codex app-server generate-json-schema --out <dir> --experimental",
28 )
29}
30
31pub fn regen(gen_dir: &Path) -> Result<()> {
32 let version = capture_codex_version()?;
40
41 let (combined, combined_defs) = load_combined_defs(gen_dir)?;
42
43 let protocol_text =
44 serde_json::to_string_pretty(&combined).context("serialize combined schema")?;
45 assert_no_v2_refs(&protocol_text)?;
46
47 let manifest = merge::build_methods_manifest(&combined_defs)?;
48 let methods_text =
56 serde_json::to_string_pretty(&manifest).context("serialize methods manifest")?;
57
58 fs::write(PROTOCOL_SCHEMA_PATH, &protocol_text)
59 .with_context(|| format!("write {PROTOCOL_SCHEMA_PATH}"))?;
60 fs::write(METHODS_JSON_PATH, &methods_text)
61 .with_context(|| format!("write {METHODS_JSON_PATH}"))?;
62
63 eprintln!("total definitions: {}", combined_defs.len());
64 eprintln!("wrote {PROTOCOL_SCHEMA_PATH}");
65
66 let missing_response: Vec<&str> = manifest
67 .client_requests
68 .iter()
69 .chain(&manifest.server_requests)
70 .filter(|e| e.response_type.is_none())
71 .map(|e| e.method.as_str())
72 .collect();
73 eprintln!(
74 "client_requests={} server_requests={} server_notifications={} client_notifications={}",
75 manifest.client_requests.len(),
76 manifest.server_requests.len(),
77 manifest.server_notifications.len(),
78 manifest.client_notifications.len(),
79 );
80 eprintln!("methods with no resolvable response type: {missing_response:?}");
81 eprintln!("wrote {METHODS_JSON_PATH}");
82
83 stamp_codex_version(&version)?;
84
85 Ok(())
86}
87
88fn assert_no_v2_refs(serialized_protocol_schema: &str) -> Result<()> {
93 let count = serialized_protocol_schema
94 .matches("#/definitions/v2/")
95 .count();
96 eprintln!("remaining v2-prefixed refs (must be 0): {count}");
97 if count != 0 {
98 bail!("ref rewrite incomplete: found {count} remaining \"#/definitions/v2/\" ref(s)");
99 }
100 Ok(())
101}
102
103fn stamp_codex_version(version: &str) -> Result<()> {
104 fs::write(CODEX_VERSION_PATH, format!("{version}\n"))
105 .with_context(|| format!("write {CODEX_VERSION_PATH}"))?;
106 eprintln!("stamped {CODEX_VERSION_PATH}: {version:?}");
107 Ok(())
108}
109
110fn capture_codex_version() -> Result<String> {
111 let output = Command::new("codex")
112 .arg("--version")
113 .output()
114 .context("failed to run `codex --version` - is the codex CLI installed and on PATH?")?;
115 if !output.status.success() {
116 bail!(
117 "`codex --version` exited with status {}: {}",
118 output.status,
119 String::from_utf8_lossy(&output.stderr)
120 );
121 }
122 let raw =
123 String::from_utf8(output.stdout).context("`codex --version` emitted non-UTF-8 output")?;
124 let trimmed = raw.trim();
125 if trimmed.is_empty() {
126 bail!("`codex --version` produced no output");
127 }
128 Ok(trimmed.to_string())
129}