diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4946978 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + push: + pull_request: + +jobs: + test: + name: Build and Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Set up Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + + - name: Run tests + run: cargo test --workspace --all-features --verbose diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml new file mode 100644 index 0000000..4de6ddb --- /dev/null +++ b/.github/workflows/release-plz.yml @@ -0,0 +1,52 @@ +name: Release-plz + +on: + push: + branches: + - main + +jobs: + release-plz-release: + name: Release-plz release + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'symposium-dev' }} + permissions: + contents: write + id-token: write + steps: + - &checkout + name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: true + token: ${{ secrets.RELEASE_PLZ_TOKEN }} + - &install-rust + name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Run release-plz + uses: release-plz/action@v0.5 + with: + command: release + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }} + + release-plz-pr: + name: Release-plz PR + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'symposium-dev' }} + permissions: + pull-requests: write + contents: write + concurrency: + group: release-plz-${{ github.ref }} + cancel-in-progress: false + steps: + - *checkout + - *install-rust + - name: Run release-plz + uses: release-plz/action@v0.5 + with: + command: release-pr + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }} diff --git a/Cargo.lock b/Cargo.lock index d8a3f0f..1ca1ec2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -431,12 +431,6 @@ dependencies = [ "syn", ] -[[package]] -name = "dissimilar" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8975ffdaa0ef3661bfe02dbdcc06c9f829dfafe6a3c474de366a8d5e44276921" - [[package]] name = "dyn-clone" version = "1.0.20" @@ -483,16 +477,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "expect-test" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63af43ff4431e848fb47472a920f14fa71c24de13255a5692e93d4e90302acb0" -dependencies = [ - "dissimilar", - "once_cell", -] - [[package]] name = "fastrand" version = "2.3.0" @@ -1257,6 +1241,12 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + [[package]] name = "pin-project" version = "1.1.10" @@ -2011,8 +2001,8 @@ version = "0.2.0" dependencies = [ "anyhow", "elizacp", - "expect-test", "indoc", + "pico-args", "sacp", "sacp-conductor", "sacp-tokio", @@ -2021,6 +2011,7 @@ dependencies = [ "serde_json", "tempfile", "tokio", + "tokio-util", "tracing", "tracing-subscriber", "yopo", @@ -2206,9 +2197,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", diff --git a/Cargo.toml b/Cargo.toml index aae6643..8eded09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,18 +11,19 @@ readme = "README.md" [dependencies] anyhow = "1.0.100" indoc = "2.0.7" +pico-args = "0.5.0" sacp = "10.0.0" sacp-tokio = "10.0.0" schemars = "1.1.0" serde = "1.0.228" serde_json = "1.0.145" tokio = { version = "1.0", features = ["macros", "rt-multi-thread", "process"] } +tokio-util = "0.7.18" tracing = "0.1.44" tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } [dev-dependencies] elizacp = "10.0.0" yopo = "10.0.0" -expect-test = "1.5" sacp-conductor = "10.0.0" tempfile = "3.6" diff --git a/src/cargo_mcp.rs b/src/cargo_mcp.rs index 65fe8e9..8788115 100644 --- a/src/cargo_mcp.rs +++ b/src/cargo_mcp.rs @@ -1,6 +1,6 @@ use crate::cargo_command::execute_cargo_command; use sacp::{ - ProxyToConductor, + JrLink, mcp_server::{McpContext, McpServer}, }; use schemars::JsonSchema; @@ -80,9 +80,9 @@ struct CargoUpdateInputs { pub extra_args: Option>, } -pub fn build_mcp_server( +pub fn build_mcp_server( cwd: Arc>>, -) -> McpServer> { +) -> McpServer> { McpServer::builder("cargo-mcp".to_string()) .instructions(indoc::indoc! {" Run cargo commands. When possible, always use this instead of calling a shell command. Generally, it makes @@ -96,7 +96,7 @@ pub fn build_mcp_server( "#}, { let cwd = cwd.clone(); - async move |input: SetCwdInputs, _mcp_cx: McpContext| { + async move |input: SetCwdInputs, _mcp_cx: McpContext| { *cwd.write().await = input.cwd.clone(); Ok(SetCwdResult { @@ -113,7 +113,7 @@ pub fn build_mcp_server( "#}, { let cwd = cwd.clone(); - async move |input: CargoCommandInputs, _mcp_cx: McpContext| { + async move |input: CargoCommandInputs, _mcp_cx: McpContext| { let cwd = if input.cwd.is_some() { input.cwd } else { @@ -132,7 +132,7 @@ pub fn build_mcp_server( "#}, { let cwd = cwd.clone(); - async move |input: CargoCommandInputs, _mcp_cx: McpContext| { + async move |input: CargoCommandInputs, _mcp_cx: McpContext| { let cwd = if input.cwd.is_some() { input.cwd } else { @@ -151,7 +151,7 @@ pub fn build_mcp_server( "#}, { let cwd = cwd.clone(); - async move |input: CargoTestInputs, _mcp_cx: McpContext| { + async move |input: CargoTestInputs, _mcp_cx: McpContext| { let args = if let Some(test_arg) = input.test_arg.as_deref() { vec![test_arg] } else { @@ -176,7 +176,7 @@ pub fn build_mcp_server( "#}, { let cwd = cwd.clone(); - async move |input: CargoAddInputs, _mcp_cx: McpContext| { + async move |input: CargoAddInputs, _mcp_cx: McpContext| { let mut args: Vec<&str> = Vec::new(); args.push(&input.package); if let Some(extra) = &input.extra_args { @@ -201,7 +201,7 @@ pub fn build_mcp_server( "#}, { let cwd = cwd.clone(); - async move |input: CargoCleanInputs, _mcp_cx: McpContext| { + async move |input: CargoCleanInputs, _mcp_cx: McpContext| { let mut args: Vec<&str> = Vec::new(); if let Some(extra) = &input.extra_args { args.extend(extra.iter().map(|s| s.as_str())); @@ -225,7 +225,7 @@ pub fn build_mcp_server( "#}, { let cwd = cwd.clone(); - async move |input: CargoRemoveInputs, _mcp_cx: McpContext| { + async move |input: CargoRemoveInputs, _mcp_cx: McpContext| { let mut args: Vec<&str> = Vec::new(); args.push(&input.package); if let Some(extra) = &input.extra_args { @@ -250,7 +250,7 @@ pub fn build_mcp_server( "#}, { let cwd = cwd.clone(); - async move |input: CargoRunInputs, _mcp_cx: McpContext| { + async move |input: CargoRunInputs, _mcp_cx: McpContext| { let mut args: Vec<&str> = Vec::new(); if input.release.unwrap_or(false) { args.push("--release"); @@ -277,7 +277,7 @@ pub fn build_mcp_server( "#}, { let cwd = cwd.clone(); - async move |input: CargoUpdateInputs, _mcp_cx: McpContext| { + async move |input: CargoUpdateInputs, _mcp_cx: McpContext| { let mut args: Vec<&str> = Vec::new(); if let Some(pkg) = input.package.as_deref() { args.push("-p"); diff --git a/src/lib.rs b/src/lib.rs index f0279b5..f047aae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,7 @@ impl Component for CargoProxy { async fn serve(self, client: impl Component) -> Result<(), sacp::Error> { let cwd = Arc::new(RwLock::new(self.workspace_path)); ProxyToConductor::builder() - .name("cargo-proxy") + .name("cargo-proxy") .with_mcp_server(build_mcp_server(cwd.clone())) .on_receive_request_from( ClientPeer, diff --git a/src/main.rs b/src/main.rs index bafdfc5..54d3822 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,13 @@ +use std::sync::Arc; + use anyhow::Result; -use sacp::component::Component; -use symposium_cargo::CargoProxy; +use pico_args::Arguments; +use sacp::{ByteStreams, component::Component}; +use symposium_cargo::{CargoProxy, build_mcp_server}; +use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt}; /// Run the proxy as a standalone binary connected to stdio -pub async fn run() -> Result<()> { - // Initialize tracing - tracing_subscriber::fmt() - .with_env_filter( - tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), - ) - .init(); - - tracing::info!("Starting cargo-proxy"); - +pub async fn run_proxy() -> Result<()> { CargoProxy::default() .serve(sacp_tokio::Stdio::new()) .await?; @@ -21,7 +15,37 @@ pub async fn run() -> Result<()> { Ok(()) } +pub async fn run_mcp() -> Result<()> { + let mcp = build_mcp_server(Arc::default()); + let stido = ByteStreams::new( + tokio::io::stdout().compat_write(), + tokio::io::stdin().compat(), + ); + mcp.serve(stido).await?; + + Ok(()) +} + #[tokio::main] async fn main() -> Result<()> { - run().await + // Initialize tracing + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("debug")), + ) + .init(); + + let mut args = Arguments::from_env(); + let mcp = args.contains("--mcp"); + let proxy = args.contains("--proxy"); + + match (mcp, proxy) { + (true, false) => run_mcp().await, + (false, true) => run_proxy().await, + _ => { + eprintln!("Usage: symposium-cargo [--mcp | --proxy]"); + std::process::exit(1); + } + } } diff --git a/tests/basic_integration.rs b/tests/basic_integration.rs index e3511ac..f55cc41 100644 --- a/tests/basic_integration.rs +++ b/tests/basic_integration.rs @@ -1,5 +1,4 @@ use anyhow::Result; -use expect_test::expect; use sacp_conductor::{Conductor, ProxiesAndAgent}; use std::path::PathBuf; use symposium_cargo::CargoProxy; @@ -43,25 +42,19 @@ async fn run_cargo_tool(tool: &str) -> Result { #[tokio::test] async fn test_cargo_check() -> Result<()> { let response = run_cargo_tool("cargo_check").await?; - - expect![[r#"OK: CallToolResult { content: [Annotated { raw: Text(RawTextContent { text: "{\"build_success\":false,\"command\":\"cargo check --message-format json\",\"exit_code\":101,\"messages\":[\"error[E0425]: cannot find value `error` in this scope\\n --> src/main.rs:2:5\\n |\\n2 | error\\n | ^^^^^ not found in this scope\\n\\n\",\"For more information about this error, try `rustc --explain E0425`.\\n\"],\"stderr\":\" Checking test-project v0.1.0 (/home/gh-jackh726/symposium/symposium-cargo/tests/test-project)\\nerror: could not compile `test-project` (bin \\\"test-project\\\") due to 1 previous error\"}", meta: None }), annotations: None }], structured_content: Some(Object {"build_success": Bool(false), "command": String("cargo check --message-format json"), "exit_code": Number(101), "messages": Array [String("error[E0425]: cannot find value `error` in this scope\n --> src/main.rs:2:5\n |\n2 | error\n | ^^^^^ not found in this scope\n\n"), String("For more information about this error, try `rustc --explain E0425`.\n")], "stderr": String(" Checking test-project v0.1.0 (/home/gh-jackh726/symposium/symposium-cargo/tests/test-project)\nerror: could not compile `test-project` (bin \"test-project\") due to 1 previous error")}), is_error: Some(false), meta: None }"#]].assert_eq(&response); - + assert!(response.contains("cannot find value `error` in this scope")); Ok(()) } #[tokio::test] async fn test_cargo_build() -> Result<()> { let response = run_cargo_tool("cargo_build").await?; - - expect![[r#"OK: CallToolResult { content: [Annotated { raw: Text(RawTextContent { text: "{\"build_success\":false,\"command\":\"cargo build --message-format json\",\"exit_code\":101,\"messages\":[\"error[E0425]: cannot find value `error` in this scope\\n --> src/main.rs:2:5\\n |\\n2 | error\\n | ^^^^^ not found in this scope\\n\\n\",\"For more information about this error, try `rustc --explain E0425`.\\n\"],\"stderr\":\" Compiling test-project v0.1.0 (/home/gh-jackh726/symposium/symposium-cargo/tests/test-project)\\nerror: could not compile `test-project` (bin \\\"test-project\\\") due to 1 previous error\"}", meta: None }), annotations: None }], structured_content: Some(Object {"build_success": Bool(false), "command": String("cargo build --message-format json"), "exit_code": Number(101), "messages": Array [String("error[E0425]: cannot find value `error` in this scope\n --> src/main.rs:2:5\n |\n2 | error\n | ^^^^^ not found in this scope\n\n"), String("For more information about this error, try `rustc --explain E0425`.\n")], "stderr": String(" Compiling test-project v0.1.0 (/home/gh-jackh726/symposium/symposium-cargo/tests/test-project)\nerror: could not compile `test-project` (bin \"test-project\") due to 1 previous error")}), is_error: Some(false), meta: None }"#]].assert_eq(&response); - + assert!(response.contains("cannot find value `error` in this scope")); Ok(()) } #[tokio::test] async fn test_cargo_test() -> Result<()> { let response = run_cargo_tool("cargo_test").await?; - - expect![[r#"OK: CallToolResult { content: [Annotated { raw: Text(RawTextContent { text: "{\"build_success\":false,\"command\":\"cargo test --message-format json\",\"exit_code\":101,\"messages\":[\"error[E0425]: cannot find value `error` in this scope\\n --> src/main.rs:2:5\\n |\\n2 | error\\n | ^^^^^ not found in this scope\\n\\n\",\"For more information about this error, try `rustc --explain E0425`.\\n\"],\"stderr\":\" Compiling test-project v0.1.0 (/home/gh-jackh726/symposium/symposium-cargo/tests/test-project)\\nerror: could not compile `test-project` (bin \\\"test-project\\\" test) due to 1 previous error\"}", meta: None }), annotations: None }], structured_content: Some(Object {"build_success": Bool(false), "command": String("cargo test --message-format json"), "exit_code": Number(101), "messages": Array [String("error[E0425]: cannot find value `error` in this scope\n --> src/main.rs:2:5\n |\n2 | error\n | ^^^^^ not found in this scope\n\n"), String("For more information about this error, try `rustc --explain E0425`.\n")], "stderr": String(" Compiling test-project v0.1.0 (/home/gh-jackh726/symposium/symposium-cargo/tests/test-project)\nerror: could not compile `test-project` (bin \"test-project\" test) due to 1 previous error")}), is_error: Some(false), meta: None }"#]].assert_eq(&response); - + assert!(response.contains("cannot find value `error` in this scope")); Ok(()) }