Skip to main content

xtask/
main.rs

1//! xtask — Repo automation for soma.
2//!
3//! Invoked via: `cargo xtask <command>`
4//!
5//! Commands:
6//!   dist         Build release binary into Cargo target dir
7//!   ci           Run all CI checks: fmt, clippy, nextest, taplo, audit
8//!   symlink-docs Create AGENTS.md and GEMINI.md symlinks next to every CLAUDE.md
9//!   check-env    Validate required environment variables are set
10//!   check-architecture Validate workspace dependency-layer boundaries
11//!   patterns     Check static contracts from docs/PATTERNS.md
12//!   contract-audit Run local static/spec checks for REST-client MCP servers
13//!   scaffold     Plan, generate, or verify a new project from Soma
14//!   codex-schema Rebuild/bisect/diff the vendored codex-app-server-client schema
15//!   cargo-generate Smoke-test cargo-generate output
16//!   cargo-generate-post Apply cargo-generate post-processing rewrites
17//!   generate-docs Generate volatile docs and metadata from canonical specs
18//!   doc           Generate Rust API documentation (rustdoc) for workspace crates
19//!   generate-provider-surfaces Generate provider docs and marketplace catalogs
20//!   check-docs    Validate generated docs and metadata are current
21//!   check-mcp-registry Validate server.json against the MCP registry schema
22//!   check-stale-claims Fail on stale hardcoded Soma claims
23//!   sync-web-source Copy apps/web into the bundled soma-web scaffold source
24//!   check-web-source-sync Validate bundled web source matches apps/web
25//!   update-aurora-web Refresh Aurora components, validate apps/web, then sync bundle
26//!   block-env-commits Prevent staged .env secrets from being committed
27//!   check-coupled-files Check common companion-file drift in a diff
28//!   check-file-size Check staged source files against size budgets
29//!   run-ascii-check Check or fix tracked source/config/docs ASCII hygiene
30//!   check-plugin-stdio-smoke Smoke-test installed plugin stdio binary
31//!   apply-no-mcp-marketplace Apply deterministic no-MCP marketplace branch transform
32//!   check-no-mcp-drift Validate marketplace-no-mcp branch invariants and drift
33//!   check-ts-client Regenerate/verify the checked-in codex-app-server-client TypeScript REST client
34//!   sync-cargo   Copy Cargo.lock into plugin data directories
35//!   check-release-versions Validate release component version policy
36//!   release-plan Print changed release components and candidate tags
37//!   sync-release-please-version Sync release files to .release-please-manifest.json
38//!   bump-version Bump a release component version
39//!   changed-paths Classify changed files into CI routing categories
40//!
41//! CUSTOMIZE: Add your own commands by adding arms to the match block below.
42//!           Keep each command as a separate `fn` for readability.
43//!
44//! Philosophy: xtask replaces ad-hoc shell scripts. It gets type-checked by the
45//! compiler, works cross-platform, and is easy to extend. Keep functions small
46//! and use `std::process::Command` to shell out to existing tools rather than
47//! reimplementing them in Rust.
48
49use 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    // Cargo sets CARGO_MANIFEST_DIR for the workspace root when invoked as
81    // `cargo xtask`. Change into the workspace root so all commands work
82    // regardless of the cwd from which the user invoked cargo.
83    //
84    // CUSTOMIZE: This path navigation assumes xtask/ is a direct child of the
85    //           workspace root. If you restructure, adjust the `..` accordingly.
86    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
172// =============================================================================
173// cargo-generate — Smoke-test generated scaffold output
174// =============================================================================
175
176fn cargo_generate(args: &[String]) -> Result<()> {
177    cargo_generate::run(args)
178}
179
180// =============================================================================
181// patterns — Check docs/PATTERNS.md contracts
182// =============================================================================
183
184fn 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
200// =============================================================================
201// Helpers
202// =============================================================================
203
204/// Run a `cargo` subcommand, forwarding stdout/stderr.
205pub(crate) fn run_cargo(args: &[&str]) -> Result<()> {
206    run_cmd("cargo", args)
207}
208
209/// Run an arbitrary command, forwarding stdout/stderr. Fails if exit code != 0.
210pub(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
222/// Run an arbitrary command and return stdout. Fails if exit code != 0.
223pub(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
240/// Check whether a cargo subcommand (or standalone binary) is installed.
241///
242/// Checks for both `cargo-nextest` (cargo subcommand) and `nextest` in PATH.
243pub(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}