1use anyhow::{bail, Context, Result};
50use std::process::{Command, Stdio};
51
52mod architecture;
53mod architecture_graph;
54mod cargo_generate;
55mod cargo_generate_post;
56mod ci_paths;
57mod codex_schema;
58mod doc_site;
59mod generated_surfaces;
60mod mcp_registry;
61mod no_mcp;
62mod patterns;
63mod provider_manifest;
64mod release_commands;
65mod release_versions;
66mod rmcp_release_monitor;
67mod scaffold;
68mod scripts;
69mod scripts_lane_a;
70mod scripts_lane_b;
71mod scripts_lane_c;
72mod scripts_lane_d;
73mod test_siblings;
74mod trace_headers_smoke;
75mod ts_client;
76mod web_source;
77mod workspace_commands;
78
79fn main() -> Result<()> {
80 let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
87 .parent()
88 .expect("xtask/Cargo.toml must have a parent directory");
89 std::env::set_current_dir(workspace_root).context("Failed to change into workspace root")?;
90
91 let args: Vec<String> = std::env::args().skip(1).collect();
92 match args.first().map(String::as_str) {
93 Some("dist") => workspace_commands::dist(),
94 Some("ci") => workspace_commands::ci(),
95 Some("symlink-docs") => workspace_commands::symlink_docs(),
96 Some("check-env") => workspace_commands::check_env(),
97 Some("patterns") => patterns_cmd(&args[1..]),
98 Some("contract-audit") => workspace_commands::contract_audit(),
99 Some("scaffold") => scaffold::run(&args[1..]),
100 Some("codex-schema") => codex_schema::run(&args[1..]),
101 Some("cargo-generate") => cargo_generate(&args[1..]),
102 Some("cargo-generate-post") => cargo_generate_post::run(&args[1..]),
103 Some("generate-docs") => workspace_commands::generate_docs(),
104 Some("doc") => workspace_commands::doc(&args[1..]),
105 Some("generate-provider-surfaces") => generated_surfaces::provider_surfaces(&args[1..]),
106 Some("check-docs") => workspace_commands::check_docs(),
107 Some("check-architecture") => architecture::check(workspace_root),
108 Some("check-mcp-registry") => mcp_registry::check_cmd(workspace_root, &args[1..]),
109 Some("check-stale-claims") => workspace_commands::check_stale_claims(),
110 Some("check-cargo-generate") => scripts_lane_d::check_cargo_generate(&args[1..]),
111 Some("sync-web-source") => web_source::sync(),
112 Some("check-web-source-sync") => web_source::check(),
113 Some("update-aurora-web") => web_source::update_aurora(),
114 Some("build-web") => scripts_lane_a::build_web(),
115 Some("web-watch") => scripts_lane_a::web_watch(),
116 Some("generate-cli") => scripts_lane_a::generate_cli(),
117 Some("repair") => scripts_lane_a::repair(),
118 Some("test-mcp-auth") => scripts_lane_a::test_mcp_auth(&args[1..]),
119 Some("test-trace-headers") => trace_headers_smoke::test_trace_headers(&args[1..]),
120 Some("block-env-commits") => scripts::block_env_commits(),
121 Some("asciicheck") => scripts_lane_d::asciicheck(&args[1..]),
122 Some("check-blob-size") => scripts_lane_c::check_blob_size(&args[1..]),
123 Some("check-coupled-files") => scripts::check_coupled_files(&args[1..]),
124 Some("check-dependency-updates") => scripts_lane_c::check_dependency_updates(&args[1..]),
125 Some("check-file-size") => scripts::check_file_size(),
126 Some("check-openapi") => scripts_lane_d::check_openapi(&args[1..]),
127 Some("check-openapi-drift") => scripts_lane_d::check_openapi(&args[1..]),
128 Some("check-ts-client") => ts_client::run(&args[1..]),
129 Some("check-palette-manifest") => generated_surfaces::check_palette_manifest(&args[1..]),
130 Some("check-provider-manifest-contract") => provider_manifest::check(),
131 Some("check-plugin-hook-contract") => {
132 scripts_lane_c::check_plugin_hook_contract(&args[1..])
133 }
134 Some("run-ascii-check") => scripts::run_ascii_check(&args[1..]),
135 Some("check-plugin-stdio-smoke") => scripts::check_plugin_stdio_smoke(),
136 Some("check-runtime-current") => scripts_lane_c::check_runtime_current(&args[1..]),
137 Some("check-schema-docs") => scripts_lane_d::check_schema_docs(&args[1..]),
138 Some("check-scaffold-intent-contract") => scripts_lane_d::check_scaffold_intent_contract(),
139 Some("apply-no-mcp-marketplace") => no_mcp::apply_cmd(),
140 Some("check-no-mcp-drift") => no_mcp::check_cmd(&args[1..]),
141 Some("sync-cargo") => scripts::sync_cargo(),
142 Some("pre-release-check") => scripts_lane_b::pre_release_check(&args[1..]),
143 Some("refresh-docs") => scripts_lane_c::refresh_docs(&args[1..]),
144 Some("test-soma-features") => scripts_lane_b::test_soma_features(workspace_root),
145 Some("validate-plugin-layout") => {
146 let plugin_root = std::env::var_os("PLUGIN_ROOT").map(std::path::PathBuf::from);
147 scripts_lane_b::validate_plugin_layout(workspace_root, plugin_root.as_deref())
148 }
149 Some("check-test-siblings") => test_siblings::check(),
150 Some("check-version-sync") => {
151 scripts_lane_b::check_version_sync(workspace_root, &args[1..])
152 }
153 Some("check-release-versions") => release_commands::check(workspace_root, &args[1..]),
154 Some("release-plan") => release_commands::plan(workspace_root, &args[1..]),
155 Some("sync-release-please-version") => {
156 release_versions::sync_release_please_version(workspace_root, "soma")
157 }
158 Some("rmcp-release-monitor") => rmcp_release_monitor::run(&args[1..]),
159 Some("bump-version") => release_commands::bump(workspace_root, &args[1..]),
160 Some("bump-soma-version") => scripts_lane_b::bump_version(workspace_root, &args[1..]),
161 Some("changed-paths") => ci_paths::run(&args[1..]),
162 Some("--help") | Some("-h") | Some("help") | None => {
163 workspace_commands::print_help();
164 Ok(())
165 }
166 Some(unknown) => {
167 bail!("Unknown xtask command: {unknown:?}\nRun `cargo xtask --help` for usage.")
168 }
169 }
170}
171
172fn cargo_generate(args: &[String]) -> Result<()> {
177 cargo_generate::run(args)
178}
179
180fn patterns_cmd(args: &[String]) -> Result<()> {
185 let mut options = patterns::PatternOptions::default();
186 for arg in args {
187 match arg.as_str() {
188 "--strict" => options.strict = true,
189 "--json" => options.json = true,
190 "--help" | "-h" => {
191 println!("Usage: cargo xtask patterns [--strict] [--json]");
192 return Ok(());
193 }
194 unknown => bail!("Unknown patterns option: {unknown}"),
195 }
196 }
197 patterns::run(options)
198}
199
200pub(crate) fn run_cargo(args: &[&str]) -> Result<()> {
206 run_cmd("cargo", args)
207}
208
209pub(crate) fn run_cmd(program: &str, args: &[&str]) -> Result<()> {
211 let status = Command::new(program)
212 .args(args)
213 .stdin(Stdio::null())
214 .status()
215 .with_context(|| format!("Failed to spawn `{program}`"))?;
216 if !status.success() {
217 bail!("`{program} {}` exited with status {status}", args.join(" "));
218 }
219 Ok(())
220}
221
222pub(crate) fn run_cmd_output(program: &str, args: &[&str]) -> Result<String> {
224 let output = Command::new(program)
225 .args(args)
226 .stdin(Stdio::null())
227 .output()
228 .with_context(|| format!("Failed to spawn `{program}`"))?;
229 if !output.status.success() {
230 bail!(
231 "`{program} {}` exited with status {}",
232 args.join(" "),
233 output.status
234 );
235 }
236 String::from_utf8(output.stdout)
237 .with_context(|| format!("`{program}` emitted non-UTF-8 stdout"))
238}
239
240pub(crate) fn command_exists(name: &str) -> bool {
244 Command::new(name)
245 .arg("--version")
246 .stdout(Stdio::null())
247 .stderr(Stdio::null())
248 .status()
249 .map(|s| s.success())
250 .unwrap_or(false)
251}