From 34228f9237746013c89cd8bfba1c9b07320073dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=97=A5=E5=A4=A9?= <61126721+TrueNine@users.noreply.github.com> Date: Sat, 25 Apr 2026 10:10:19 +0800 Subject: [PATCH 1/2] fix: MCP packaging smoke missing import and cargo fmt Fix two CI failures from previous merge --- .../tests/logging_error_feedback.rs | 9 +- .../tests/logging_install_observability.rs | 4 +- cli/local-tests/tests/logging_levels.rs | 4 +- cli/src/cli.rs | 2 - mcp/integrate-tests/tests/packaging_smoke.rs | 2 + mcp/src/main.rs | 13 +- .../domain/output_plans/codex_output_plan.rs | 4 +- .../domain/output_plans/cursor_output_plan.rs | 4 +- .../domain/output_plans/droid_output_plan.rs | 4 +- .../domain/output_plans/gemini_output_plan.rs | 4 +- .../generic_skills_output_plan.rs | 4 +- .../domain/output_plans/kiro_output_plan.rs | 4 +- .../output_plans/opencode_output_plan.rs | 4 +- .../domain/output_plans/qoder_output_plan.rs | 4 +- .../domain/output_plans/trae_output_plan.rs | 4 +- .../domain/output_plans/warp_output_plan.rs | 4 +- .../output_plans/windsurf_output_plan.rs | 4 +- sdk/src/infra/git_fs.rs | 6 +- sdk/src/infra/logger/core.rs | 18 +- sdk/src/infra/logger/diagnostic.rs | 14 +- sdk/src/infra/logger/formatter.rs | 34 ++- sdk/src/infra/logger/mod.rs | 8 +- sdk/src/infra/logger/sink.rs | 17 +- sdk/src/services/clean_service.rs | 281 +++++++++++++----- sdk/src/services/common.rs | 3 +- sdk/src/services/dry_run_service.rs | 82 +++-- sdk/src/services/install_service.rs | 108 ++++--- sdk/src/services/prompt_service.rs | 10 +- 28 files changed, 461 insertions(+), 198 deletions(-) diff --git a/cli/local-tests/tests/logging_error_feedback.rs b/cli/local-tests/tests/logging_error_feedback.rs index 952bd969..160bd7ee 100644 --- a/cli/local-tests/tests/logging_error_feedback.rs +++ b/cli/local-tests/tests/logging_error_feedback.rs @@ -30,21 +30,24 @@ fn missing_config_outputs_diagnostic_with_fix() { assert!( result.stderr.contains("What happened") || result.stdout.contains("What happened"), "error should contain 'What happened' section. stdout:\n{}\nstderr:\n{}", - result.stdout, result.stderr + result.stdout, + result.stderr ); // 验证有修复建议(嵌入在错误消息中) assert!( result.stderr.contains("Please create it") || result.stdout.contains("Please create it"), "error should contain fix suggestion. stdout:\n{}\nstderr:\n{}", - result.stdout, result.stderr + result.stdout, + result.stderr ); // 验证提及配置文件 assert!( result.stderr.contains(".tnmsc.json") || result.stdout.contains(".tnmsc.json"), "error should mention .tnmsc.json. stdout:\n{}\nstderr:\n{}", - result.stdout, result.stderr + result.stdout, + result.stderr ); } diff --git a/cli/local-tests/tests/logging_install_observability.rs b/cli/local-tests/tests/logging_install_observability.rs index 1993c75f..ac9b2585 100644 --- a/cli/local-tests/tests/logging_install_observability.rs +++ b/cli/local-tests/tests/logging_install_observability.rs @@ -49,7 +49,9 @@ fn install_outputs_key_spans_and_events() { // 验证 collector span assert!( - result.stdout.contains("### collect.aindex_resolvers started"), + result + .stdout + .contains("### collect.aindex_resolvers started"), "install should output 'collect.aindex_resolvers' span. stdout:\n{}", result.stdout ); diff --git a/cli/local-tests/tests/logging_levels.rs b/cli/local-tests/tests/logging_levels.rs index b04ba3dc..fa5fc249 100644 --- a/cli/local-tests/tests/logging_levels.rs +++ b/cli/local-tests/tests/logging_levels.rs @@ -16,7 +16,9 @@ fn trace_level_outputs_span_events() { // Trace 级别应该输出 collector span assert!( - result.stdout.contains("### collect.aindex_resolvers started"), + result + .stdout + .contains("### collect.aindex_resolvers started"), "--trace should output collector spans. stdout:\n{}", result.stdout ); diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 0ab42be5..60dc4179 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -112,8 +112,6 @@ impl ResolvedLogLevel { Self::Error => "error", } } - - } /// Resolve log level from CLI flags. diff --git a/mcp/integrate-tests/tests/packaging_smoke.rs b/mcp/integrate-tests/tests/packaging_smoke.rs index 4c865f8b..bad8059b 100644 --- a/mcp/integrate-tests/tests/packaging_smoke.rs +++ b/mcp/integrate-tests/tests/packaging_smoke.rs @@ -1,3 +1,5 @@ +use std::fs; + #[cfg(unix)] use std::os::unix::fs::PermissionsExt; diff --git a/mcp/src/main.rs b/mcp/src/main.rs index 017697d5..8cc7d8cc 100644 --- a/mcp/src/main.rs +++ b/mcp/src/main.rs @@ -327,7 +327,7 @@ fn main() -> ExitCode { std::env::var("LOG_LEVEL") .ok() .and_then(|s| tnmsd::infra::logger::LogLevel::from_str_loose(&s)) - .unwrap_or(tnmsd::infra::logger::LogLevel::Info) + .unwrap_or(tnmsd::infra::logger::LogLevel::Info), ); let cli = Cli::parse(); @@ -336,10 +336,13 @@ fn main() -> ExitCode { match resolve_command(&cli) { ResolvedCommand::Serve => { let _span = logger.span("server.serve").enter(); - logger.info("MCP server started", Some(json!({ - "serverName": SERVER_NAME, - "protocolVersion": PROTOCOL_VERSION, - }))); + logger.info( + "MCP server started", + Some(json!({ + "serverName": SERVER_NAME, + "protocolVersion": PROTOCOL_VERSION, + })), + ); run_stdio_server(); ExitCode::SUCCESS } diff --git a/sdk/src/domain/output_plans/codex_output_plan.rs b/sdk/src/domain/output_plans/codex_output_plan.rs index 68eb89a0..3bd072b0 100644 --- a/sdk/src/domain/output_plans/codex_output_plan.rs +++ b/sdk/src/domain/output_plans/codex_output_plan.rs @@ -18,11 +18,11 @@ use std::path::PathBuf; use crate::CliError; -use crate::domain::output_context::OutputContext; use crate::domain::base_output_plans::{BaseOutputFileDeclarationDto, BaseOutputPluginPlanDto}; +use crate::domain::cleanup::{CleanupDeclarationsDto, CleanupTargetDto, CleanupTargetKindDto}; use crate::domain::config; +use crate::domain::output_context::OutputContext; use crate::domain::plugin_shared::{Project, RelativePath, Workspace}; -use crate::domain::cleanup::{CleanupDeclarationsDto, CleanupTargetDto, CleanupTargetKindDto}; const CODEX_PLUGIN_NAME: &str = "CodexCLIOutputAdaptor"; const CODEX_INSTRUCTIONS_FILE: &str = "AGENTS.md"; diff --git a/sdk/src/domain/output_plans/cursor_output_plan.rs b/sdk/src/domain/output_plans/cursor_output_plan.rs index 8edbfb6e..23e0c270 100644 --- a/sdk/src/domain/output_plans/cursor_output_plan.rs +++ b/sdk/src/domain/output_plans/cursor_output_plan.rs @@ -1,10 +1,10 @@ use std::path::PathBuf; use crate::CliError; -use crate::domain::output_context::OutputContext; use crate::domain::base_output_plans::{BaseOutputFileDeclarationDto, BaseOutputPluginPlanDto}; -use crate::domain::plugin_shared::{Project, RelativePath, Workspace}; use crate::domain::cleanup::{CleanupDeclarationsDto, CleanupTargetDto, CleanupTargetKindDto}; +use crate::domain::output_context::OutputContext; +use crate::domain::plugin_shared::{Project, RelativePath, Workspace}; const CURSOR_PLUGIN_NAME: &str = "CursorOutputAdaptor"; const CURSOR_MEMORY_FILE: &str = ".cursorrules"; diff --git a/sdk/src/domain/output_plans/droid_output_plan.rs b/sdk/src/domain/output_plans/droid_output_plan.rs index c0220f13..d7dafc3b 100644 --- a/sdk/src/domain/output_plans/droid_output_plan.rs +++ b/sdk/src/domain/output_plans/droid_output_plan.rs @@ -5,13 +5,13 @@ use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use crate::CliError; -use crate::domain::output_context::OutputContext; +use crate::domain::cleanup::{CleanupDeclarationsDto, CleanupTargetDto, CleanupTargetKindDto}; use crate::domain::config; +use crate::domain::output_context::OutputContext; use crate::domain::plugin_shared::{ FastCommandPrompt, Project, RelativePath, RuleScope, SkillPrompt, SkillResourceEncoding, Workspace, }; -use crate::domain::cleanup::{CleanupDeclarationsDto, CleanupTargetDto, CleanupTargetKindDto}; const DROID_PLUGIN_NAME: &str = "DroidCLIOutputAdaptor"; const DROID_MEMORY_FILE: &str = "AGENTS.md"; diff --git a/sdk/src/domain/output_plans/gemini_output_plan.rs b/sdk/src/domain/output_plans/gemini_output_plan.rs index a3ad9ea0..ea7b4c69 100644 --- a/sdk/src/domain/output_plans/gemini_output_plan.rs +++ b/sdk/src/domain/output_plans/gemini_output_plan.rs @@ -2,11 +2,11 @@ use std::collections::HashSet; use std::path::PathBuf; use crate::CliError; -use crate::domain::output_context::OutputContext; use crate::domain::base_output_plans::{BaseOutputFileDeclarationDto, BaseOutputPluginPlanDto}; +use crate::domain::cleanup::{CleanupDeclarationsDto, CleanupTargetDto, CleanupTargetKindDto}; use crate::domain::config; +use crate::domain::output_context::OutputContext; use crate::domain::plugin_shared::{Project, RelativePath, Workspace}; -use crate::domain::cleanup::{CleanupDeclarationsDto, CleanupTargetDto, CleanupTargetKindDto}; const GEMINI_PLUGIN_NAME: &str = "GeminiCLIOutputAdaptor"; const GEMINI_MEMORY_FILE: &str = "GEMINI.md"; diff --git a/sdk/src/domain/output_plans/generic_skills_output_plan.rs b/sdk/src/domain/output_plans/generic_skills_output_plan.rs index 7210563a..87626a36 100644 --- a/sdk/src/domain/output_plans/generic_skills_output_plan.rs +++ b/sdk/src/domain/output_plans/generic_skills_output_plan.rs @@ -1,8 +1,8 @@ use crate::CliError; -use crate::domain::output_context::OutputContext; use crate::domain::base_output_plans::BaseOutputPluginPlanDto; -use crate::domain::plugin_shared::Workspace; use crate::domain::cleanup::CleanupDeclarationsDto; +use crate::domain::output_context::OutputContext; +use crate::domain::plugin_shared::Workspace; const GENERIC_SKILLS_PLUGIN_NAME: &str = "GenericSkillsOutputAdaptor"; diff --git a/sdk/src/domain/output_plans/kiro_output_plan.rs b/sdk/src/domain/output_plans/kiro_output_plan.rs index 472b5095..3fc88d26 100644 --- a/sdk/src/domain/output_plans/kiro_output_plan.rs +++ b/sdk/src/domain/output_plans/kiro_output_plan.rs @@ -1,10 +1,10 @@ use std::path::PathBuf; use crate::CliError; -use crate::domain::output_context::OutputContext; use crate::domain::base_output_plans::BaseOutputPluginPlanDto; -use crate::domain::plugin_shared::{Project, RelativePath, Workspace}; use crate::domain::cleanup::{CleanupDeclarationsDto, CleanupTargetDto, CleanupTargetKindDto}; +use crate::domain::output_context::OutputContext; +use crate::domain::plugin_shared::{Project, RelativePath, Workspace}; const KIRO_PLUGIN_NAME: &str = "KiroCLIOutputAdaptor"; const PROJECT_SCOPE: &str = "project"; diff --git a/sdk/src/domain/output_plans/opencode_output_plan.rs b/sdk/src/domain/output_plans/opencode_output_plan.rs index 6bd98f61..11f34092 100644 --- a/sdk/src/domain/output_plans/opencode_output_plan.rs +++ b/sdk/src/domain/output_plans/opencode_output_plan.rs @@ -3,11 +3,11 @@ use std::path::PathBuf; use serde_json::Value; use crate::CliError; -use crate::domain::output_context::OutputContext; use crate::domain::base_output_plans::{BaseOutputFileDeclarationDto, BaseOutputPluginPlanDto}; +use crate::domain::cleanup::{CleanupDeclarationsDto, CleanupTargetDto, CleanupTargetKindDto}; use crate::domain::config; +use crate::domain::output_context::OutputContext; use crate::domain::plugin_shared::{Project, RelativePath, Workspace}; -use crate::domain::cleanup::{CleanupDeclarationsDto, CleanupTargetDto, CleanupTargetKindDto}; const OPENCODE_PLUGIN_NAME: &str = "OpencodeCLIOutputAdaptor"; const OPENCODE_MEMORY_FILE: &str = "AGENTS.md"; diff --git a/sdk/src/domain/output_plans/qoder_output_plan.rs b/sdk/src/domain/output_plans/qoder_output_plan.rs index 81fdb322..56fe563c 100644 --- a/sdk/src/domain/output_plans/qoder_output_plan.rs +++ b/sdk/src/domain/output_plans/qoder_output_plan.rs @@ -1,8 +1,8 @@ use crate::CliError; -use crate::domain::output_context::OutputContext; use crate::domain::base_output_plans::BaseOutputPluginPlanDto; -use crate::domain::plugin_shared::Workspace; use crate::domain::cleanup::CleanupDeclarationsDto; +use crate::domain::output_context::OutputContext; +use crate::domain::plugin_shared::Workspace; const QODER_PLUGIN_NAME: &str = "QoderIDEPluginOutputAdaptor"; diff --git a/sdk/src/domain/output_plans/trae_output_plan.rs b/sdk/src/domain/output_plans/trae_output_plan.rs index 6c4c9f42..353958bd 100644 --- a/sdk/src/domain/output_plans/trae_output_plan.rs +++ b/sdk/src/domain/output_plans/trae_output_plan.rs @@ -1,10 +1,10 @@ use std::path::PathBuf; use crate::CliError; -use crate::domain::output_context::OutputContext; use crate::domain::base_output_plans::{BaseOutputFileDeclarationDto, BaseOutputPluginPlanDto}; -use crate::domain::plugin_shared::{Project, RelativePath, Workspace}; use crate::domain::cleanup::{CleanupDeclarationsDto, CleanupTargetDto, CleanupTargetKindDto}; +use crate::domain::output_context::OutputContext; +use crate::domain::plugin_shared::{Project, RelativePath, Workspace}; const TRAE_PLUGIN_NAME: &str = "TraeOutputAdaptor"; const TRAE_STEERING_FILE: &str = "GLOBAL.md"; diff --git a/sdk/src/domain/output_plans/warp_output_plan.rs b/sdk/src/domain/output_plans/warp_output_plan.rs index 5a604ae9..4a9d9294 100644 --- a/sdk/src/domain/output_plans/warp_output_plan.rs +++ b/sdk/src/domain/output_plans/warp_output_plan.rs @@ -1,10 +1,10 @@ use std::path::PathBuf; use crate::CliError; -use crate::domain::output_context::OutputContext; use crate::domain::base_output_plans::{BaseOutputFileDeclarationDto, BaseOutputPluginPlanDto}; -use crate::domain::plugin_shared::{Project, RelativePath, Workspace}; use crate::domain::cleanup::{CleanupDeclarationsDto, CleanupTargetDto, CleanupTargetKindDto}; +use crate::domain::output_context::OutputContext; +use crate::domain::plugin_shared::{Project, RelativePath, Workspace}; const WARP_PLUGIN_NAME: &str = "WarpIDEOutputAdaptor"; const WARP_MEMORY_FILE: &str = "WARP.md"; diff --git a/sdk/src/domain/output_plans/windsurf_output_plan.rs b/sdk/src/domain/output_plans/windsurf_output_plan.rs index c97b302c..7eed372f 100644 --- a/sdk/src/domain/output_plans/windsurf_output_plan.rs +++ b/sdk/src/domain/output_plans/windsurf_output_plan.rs @@ -1,10 +1,10 @@ use std::path::PathBuf; use crate::CliError; -use crate::domain::output_context::OutputContext; use crate::domain::base_output_plans::{BaseOutputFileDeclarationDto, BaseOutputPluginPlanDto}; -use crate::domain::plugin_shared::{Project, RelativePath, Workspace}; use crate::domain::cleanup::{CleanupDeclarationsDto, CleanupTargetDto, CleanupTargetKindDto}; +use crate::domain::output_context::OutputContext; +use crate::domain::plugin_shared::{Project, RelativePath, Workspace}; const WINDSURF_PLUGIN_NAME: &str = "WindsurfOutputAdaptor"; const WINDSURF_MEMORY_FILE: &str = ".windsurfrules"; diff --git a/sdk/src/infra/git_fs.rs b/sdk/src/infra/git_fs.rs index 9197d21b..4a0d31df 100644 --- a/sdk/src/infra/git_fs.rs +++ b/sdk/src/infra/git_fs.rs @@ -101,7 +101,11 @@ mod tests { let result = resolve_git_info_dir(tmp.path()); assert!(result.is_some()); - let result_str = result.as_ref().unwrap().to_string_lossy().replace('\\', "/"); + let result_str = result + .as_ref() + .unwrap() + .to_string_lossy() + .replace('\\', "/"); // On Windows, absolute paths starting with / get a drive letter prefix let result_normalized = result_str .strip_prefix("C:") diff --git a/sdk/src/infra/logger/core.rs b/sdk/src/infra/logger/core.rs index 47191a45..406466f1 100644 --- a/sdk/src/infra/logger/core.rs +++ b/sdk/src/infra/logger/core.rs @@ -4,7 +4,9 @@ use std::time::{Duration, Instant}; use serde::Serialize; use serde_json::Value; -use super::diagnostic::{DiagnosticInput, invalid_record, record_from_input, validate_diagnostic_input}; +use super::diagnostic::{ + DiagnosticInput, invalid_record, record_from_input, validate_diagnostic_input, +}; use super::sink::buffer_diagnostic; // --------------------------------------------------------------------------- @@ -102,7 +104,10 @@ impl SpanGuard { fn new(span: Span) -> Self { // Emit span enter event immediately crate::infra::logger::sink::write_span_enter(&span); - Self { span, exited: false } + Self { + span, + exited: false, + } } pub fn exit(mut self) { @@ -204,9 +209,12 @@ impl Logger { fn log_diagnostic(&self, level: LogLevel, diagnostic: DiagnosticInput) { let record = match validate_diagnostic_input(&diagnostic) { Ok(()) => record_from_input(&self.namespace, level.as_str(), diagnostic), - Err(errors) => { - invalid_record(&self.namespace, level.as_str(), serde_json::to_value(&diagnostic).unwrap_or_default(), &errors) - } + Err(errors) => invalid_record( + &self.namespace, + level.as_str(), + serde_json::to_value(&diagnostic).unwrap_or_default(), + &errors, + ), }; // Buffer diagnostics even if level is Silent diff --git a/sdk/src/infra/logger/diagnostic.rs b/sdk/src/infra/logger/diagnostic.rs index 22223c71..75750b9e 100644 --- a/sdk/src/infra/logger/diagnostic.rs +++ b/sdk/src/infra/logger/diagnostic.rs @@ -50,7 +50,9 @@ pub fn validate_diagnostic_input(input: &DiagnosticInput) -> Result<(), Vec Result<(), Vec DiagnosticRecord { +pub fn record_from_input(namespace: &str, level: &str, input: DiagnosticInput) -> DiagnosticRecord { let mut record = DiagnosticRecord { code: input.code.trim().to_string(), title: input.title.trim().to_string(), diff --git a/sdk/src/infra/logger/formatter.rs b/sdk/src/infra/logger/formatter.rs index 768ba641..191941c6 100644 --- a/sdk/src/infra/logger/formatter.rs +++ b/sdk/src/infra/logger/formatter.rs @@ -5,12 +5,8 @@ use super::core::{Event, LogLevel, Span}; /// Format an event as Markdown. pub fn format_event(event: &Event) -> String { match event.level { - LogLevel::Warn | LogLevel::Error | LogLevel::Fatal => { - format_diagnostic_event(event) - } - _ => { - format_message_event(event) - } + LogLevel::Warn | LogLevel::Error | LogLevel::Fatal => format_diagnostic_event(event), + _ => format_message_event(event), } } @@ -22,7 +18,10 @@ pub fn format_span_enter(span: &Span) -> String { /// Format a span exit event with duration. pub fn format_span_exit(span: &Span) -> String { let duration_ms = span.duration().as_millis(); - format!("### {} completed\n - duration: {}ms", span.name, duration_ms) + format!( + "### {} completed\n - duration: {}ms", + span.name, duration_ms + ) } fn format_message_event(event: &Event) -> String { @@ -52,10 +51,11 @@ fn format_message_event(event: &Event) -> String { fn format_diagnostic_event(event: &Event) -> String { // For diagnostic events, the message contains the serialized DiagnosticRecord - let record: super::diagnostic::DiagnosticRecord = match serde_json::from_value(event.message.clone()) { - Ok(r) => r, - Err(_) => return "### Diagnostic error\n - failed to parse diagnostic record".to_string(), - }; + let record: super::diagnostic::DiagnosticRecord = + match serde_json::from_value(event.message.clone()) { + Ok(r) => r, + Err(_) => return "### Diagnostic error\n - failed to parse diagnostic record".to_string(), + }; let mut lines = vec![format!("### {}", record.title)]; @@ -108,7 +108,10 @@ fn format_diagnostic_event(event: &Event) -> String { lines.join("\n") } -fn extract_message_and_meta(message: &Value, meta: Option<&Value>) -> (Option, Vec) { +fn extract_message_and_meta( + message: &Value, + meta: Option<&Value>, +) -> (Option, Vec) { let (msg, mut lines) = match message { Value::String(s) => (Some(s.clone()), Vec::new()), Value::Object(map) => { @@ -159,7 +162,12 @@ pub(crate) fn value_to_markdown_lines(value: &Value) -> Vec { lines } -pub(crate) fn append_markdown_value(lines: &mut Vec, label: Option<&str>, value: &Value, depth: usize) { +pub(crate) fn append_markdown_value( + lines: &mut Vec, + label: Option<&str>, + value: &Value, + depth: usize, +) { let prefix = " ".repeat(depth); let bullet = format!("{prefix}- "); diff --git a/sdk/src/infra/logger/mod.rs b/sdk/src/infra/logger/mod.rs index 39009be4..6d773bd0 100644 --- a/sdk/src/infra/logger/mod.rs +++ b/sdk/src/infra/logger/mod.rs @@ -10,7 +10,9 @@ pub mod diagnostic; pub mod formatter; pub mod sink; -pub use core::{LogLevel, Logger, Span, SpanGuard, get_global_level, resolve_level, set_global_level}; +pub use core::{ + LogLevel, Logger, Span, SpanGuard, get_global_level, resolve_level, set_global_level, +}; pub use diagnostic::{DiagnosticInput, DiagnosticRecord, validate_diagnostic_input}; pub use sink::{clear_diagnostics, drain_diagnostics, flush}; @@ -232,7 +234,9 @@ mod tests { #[test] fn test_resolve_level_fallback_to_global() { set_global_level(LogLevel::Warn); - unsafe { std::env::remove_var("LOG_LEVEL"); } + unsafe { + std::env::remove_var("LOG_LEVEL"); + } let level = resolve_level(None); assert_eq!(level, LogLevel::Warn); } diff --git a/sdk/src/infra/logger/sink.rs b/sdk/src/infra/logger/sink.rs index 2aab9a9c..5d5c5913 100644 --- a/sdk/src/infra/logger/sink.rs +++ b/sdk/src/infra/logger/sink.rs @@ -1,5 +1,5 @@ use std::io::{self, Write}; -use std::sync::mpsc::{self, Sender, Receiver}; +use std::sync::mpsc::{self, Receiver, Sender}; use std::sync::{LazyLock, Mutex}; use std::thread; @@ -29,7 +29,10 @@ static DIAGNOSTIC_BUFFER: LazyLock>> = // --------------------------------------------------------------------------- pub fn write_event(event: &Event) { - let use_stderr = matches!(event.level, LogLevel::Error | LogLevel::Fatal | LogLevel::Warn); + let use_stderr = matches!( + event.level, + LogLevel::Error | LogLevel::Fatal | LogLevel::Warn + ); let output = formatter::format_event(event); send_output(use_stderr, output); } @@ -65,7 +68,10 @@ pub fn clear_diagnostics() { pub fn flush() { let (ack_tx, ack_rx) = mpsc::channel(); - if OUTPUT_SINK.send(OutputCommand::Flush { ack: ack_tx }).is_ok() { + if OUTPUT_SINK + .send(OutputCommand::Flush { ack: ack_tx }) + .is_ok() + { let _ = ack_rx.recv(); } } @@ -76,7 +82,10 @@ pub fn flush() { fn send_output(use_stderr: bool, output: String) { if OUTPUT_SINK - .send(OutputCommand::Write { use_stderr, output: output.clone() }) + .send(OutputCommand::Write { + use_stderr, + output: output.clone(), + }) .is_err() { // Fallback: write directly if sink thread is dead diff --git a/sdk/src/services/clean_service.rs b/sdk/src/services/clean_service.rs index b859f1fb..aa0c1bfe 100644 --- a/sdk/src/services/clean_service.rs +++ b/sdk/src/services/clean_service.rs @@ -16,7 +16,13 @@ use crate::services::common::{ use crate::{CliError, MemorySyncCommandOptions, MemorySyncCommandResult}; pub fn clean(options: MemorySyncCommandOptions) -> Result { - let logger = create_logger("clean", options.log_level.as_deref().and_then(|s| crate::infra::logger::LogLevel::from_str_loose(s))); + let logger = create_logger( + "clean", + options + .log_level + .as_deref() + .and_then(|s| crate::infra::logger::LogLevel::from_str_loose(s)), + ); let _span = logger.span("command.clean").enter(); logger.info("Clean started", None); @@ -31,36 +37,56 @@ pub fn clean(options: MemorySyncCommandOptions) -> Result Result Result Result std::io::Result<()> { + fn create_test_config( + home_dir: &std::path::Path, + workspace_dir: &std::path::Path, + ) -> std::io::Result<()> { let config_content = json!({ "workspaceDir": workspace_dir.to_string_lossy() }); @@ -874,10 +1021,8 @@ mod tests { std::fs::create_dir_all(ws.join("project-b")).unwrap(); let scope = ws.join("project-a"); - let snapshot = build_cleanup_snapshot(&ws.to_string_lossy(), - &HashMap::new(), - &HashMap::new(), - ).unwrap(); + let snapshot = + build_cleanup_snapshot(&ws.to_string_lossy(), &HashMap::new(), &HashMap::new()).unwrap(); let filtered = filter_snapshot_by_scope(snapshot, &scope, ws); diff --git a/sdk/src/services/common.rs b/sdk/src/services/common.rs index f9e23bc7..4ec68f07 100644 --- a/sdk/src/services/common.rs +++ b/sdk/src/services/common.rs @@ -3,10 +3,10 @@ use std::path::{Path, PathBuf}; use serde::de::DeserializeOwned; use serde_json::{Value, json}; +use crate::CliError; use crate::context::OutputContext; use crate::domain::config::{self, ConfigLoader, PluginsConfig, UserConfigFile}; use crate::infra::logger::Logger; -use crate::CliError; // --------------------------------------------------------------------------- // Plugin defaults @@ -386,7 +386,6 @@ pub fn collect_context( enabled_plugins: &EnabledPlugins, logger: &Logger, ) -> Result { - let aindex = { let _span = logger.span("collect.aindex_resolvers").enter(); collect_json::( diff --git a/sdk/src/services/dry_run_service.rs b/sdk/src/services/dry_run_service.rs index 6ac0777b..62043b37 100644 --- a/sdk/src/services/dry_run_service.rs +++ b/sdk/src/services/dry_run_service.rs @@ -22,7 +22,13 @@ struct PlannedOutputFile { } pub fn dry_run(options: MemorySyncCommandOptions) -> Result { - let logger = create_logger("dry_run", options.log_level.as_deref().and_then(|s| crate::infra::logger::LogLevel::from_str_loose(s))); + let logger = create_logger( + "dry_run", + options + .log_level + .as_deref() + .and_then(|s| crate::infra::logger::LogLevel::from_str_loose(s)), + ); let _span = logger.span("command.dry_run").enter(); logger.info("Dry run started", None); @@ -39,28 +45,45 @@ pub fn dry_run(options: MemorySyncCommandOptions) -> Result Result std::io::Result<()> { + fn create_test_config( + home_dir: &std::path::Path, + workspace_dir: &std::path::Path, + ) -> std::io::Result<()> { let config_content = json!({ "workspaceDir": workspace_dir.to_string_lossy() }); diff --git a/sdk/src/services/install_service.rs b/sdk/src/services/install_service.rs index 7b951233..bc12deaa 100644 --- a/sdk/src/services/install_service.rs +++ b/sdk/src/services/install_service.rs @@ -27,12 +27,21 @@ struct PlannedOutputFile { pub(crate) fn install( options: MemorySyncCommandOptions, ) -> Result { - let logger = create_logger("install", options.log_level.as_deref().and_then(|s| crate::infra::logger::LogLevel::from_str_loose(s))); + let logger = create_logger( + "install", + options + .log_level + .as_deref() + .and_then(|s| crate::infra::logger::LogLevel::from_str_loose(s)), + ); let _span = logger.span("command.install").enter(); - logger.info("Install started", Some(json!({ - "cwd": options.cwd.as_ref(), - }))); + logger.info( + "Install started", + Some(json!({ + "cwd": options.cwd.as_ref(), + })), + ); let cwd = resolve_cwd(options.cwd.as_deref())?; @@ -46,37 +55,57 @@ pub(crate) fn install( .collect::>(); let workspace_dir_str = workspace_dir.to_string_lossy().into_owned(); - logger.info("Config loaded", Some(json!({ - "workspaceDir": &workspace_dir_str, - "configFound": config_result.found, - "configSources": config_result.sources, - }))); + logger.info( + "Config loaded", + Some(json!({ + "workspaceDir": &workspace_dir_str, + "configFound": config_result.found, + "configSources": config_result.sources, + })), + ); let global_scope = crate::services::common::build_global_scope(&config_result.config); - let enabled_plugins = EnabledPlugins::from_config(config_result.config.plugins.as_ref(), DefaultPluginKind::Install); - - logger.info("Plugins resolved", Some(json!({ - "enabled": enabled_plugins.registered_plugins(), - }))); + let enabled_plugins = EnabledPlugins::from_config( + config_result.config.plugins.as_ref(), + DefaultPluginKind::Install, + ); + + logger.info( + "Plugins resolved", + Some(json!({ + "enabled": enabled_plugins.registered_plugins(), + })), + ); let context_span = logger.span("context.collect").enter(); - let context = collect_context(&workspace_dir_str, global_scope.as_ref(), &enabled_plugins, &logger)?; + let context = collect_context( + &workspace_dir_str, + global_scope.as_ref(), + &enabled_plugins, + &logger, + )?; context_span.exit(); - logger.info("Context collected", Some(json!({ - "globalMemory": context.global_memory.is_some(), - "commands": context.fast_commands.as_ref().map(|v| v.len()), - "skills": context.skills.as_ref().map(|v| v.len()), - "rules": context.rules.as_ref().map(|v| v.len()), - }))); + logger.info( + "Context collected", + Some(json!({ + "globalMemory": context.global_memory.is_some(), + "commands": context.fast_commands.as_ref().map(|v| v.len()), + "skills": context.skills.as_ref().map(|v| v.len()), + "rules": context.rules.as_ref().map(|v| v.len()), + })), + ); let output_span = logger.span("output.build").enter(); let planned_outputs = build_output_files(&context, enabled_plugins, &logger)?; output_span.exit(); - logger.info("Output files built", Some(json!({ - "filesPlanned": planned_outputs.len(), - }))); + logger.info( + "Output files built", + Some(json!({ + "filesPlanned": planned_outputs.len(), + })), + ); let write_span = logger.span("files.write").enter(); let execution = write_output_files(&planned_outputs, &logger)?; @@ -84,13 +113,16 @@ pub(crate) fn install( warnings.extend(execution.warnings); - logger.info("Install completed", Some(json!({ - "success": execution.errors.is_empty(), - "filesAffected": execution.files_affected, - "dirsAffected": execution.dirs_affected, - "warnings": warnings.len(), - "errors": execution.errors.len(), - }))); + logger.info( + "Install completed", + Some(json!({ + "success": execution.errors.is_empty(), + "filesAffected": execution.files_affected, + "dirsAffected": execution.dirs_affected, + "warnings": warnings.len(), + "errors": execution.errors.len(), + })), + ); Ok(MemorySyncCommandResult { success: execution.errors.is_empty(), @@ -125,7 +157,8 @@ fn build_output_files( if enabled_plugins.claude_code { let plugin_span = logger.span("output.claude_code").enter(); - let plan = crate::domain::output_plans::claude_code_output_plan::build_claude_code_output_plan(context)?; + let plan = + crate::domain::output_plans::claude_code_output_plan::build_claude_code_output_plan(context)?; push_base_output_files(&mut outputs, &plan.output_files); plugin_span.exit(); } @@ -167,7 +200,8 @@ fn build_output_files( } if enabled_plugins.opencode { let plugin_span = logger.span("output.opencode").enter(); - let plan = crate::domain::output_plans::opencode_output_plan::build_opencode_output_plan(context)?; + let plan = + crate::domain::output_plans::opencode_output_plan::build_opencode_output_plan(context)?; push_base_output_files(&mut outputs, &plan.output_files); plugin_span.exit(); } @@ -191,7 +225,8 @@ fn build_output_files( } if enabled_plugins.windsurf { let plugin_span = logger.span("output.windsurf").enter(); - let plan = crate::domain::output_plans::windsurf_output_plan::build_windsurf_output_plan(context)?; + let plan = + crate::domain::output_plans::windsurf_output_plan::build_windsurf_output_plan(context)?; push_base_output_files(&mut outputs, &plan.output_files); plugin_span.exit(); } @@ -288,7 +323,10 @@ fn write_output_files( let existing = fs::read(path).ok(); if existing.as_deref() == Some(bytes.as_slice()) { - logger.debug(format!("file.skipped: {}", file.path), Some(json!({ "reason": "unchanged" }))); + logger.debug( + format!("file.skipped: {}", file.path), + Some(json!({ "reason": "unchanged" })), + ); continue; } diff --git a/sdk/src/services/prompt_service.rs b/sdk/src/services/prompt_service.rs index a7415091..93afadad 100644 --- a/sdk/src/services/prompt_service.rs +++ b/sdk/src/services/prompt_service.rs @@ -1092,7 +1092,10 @@ pub fn get_prompt( let def = build_prompt_definition_from_id(prompt_id, &env)?; let result = hydrate_prompt(&def, true); - logger.info(format!("Get prompt: {}", prompt_id), Some(serde_json::json!({ "found": result.is_some() }))); + logger.info( + format!("Get prompt: {}", prompt_id), + Some(serde_json::json!({ "found": result.is_some() })), + ); Ok(result) } @@ -1113,7 +1116,10 @@ pub fn upsert_prompt_source(input: &UpsertPromptSourceInput) -> Result Date: Wed, 29 Apr 2026 20:35:06 -0700 Subject: [PATCH 2/2] fix(mcp): route assemble-npm output to stderr to keep stdout protocol-safe (#225) Signed-off-by: SAY-5 --- mcp/src/commands/package.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mcp/src/commands/package.rs b/mcp/src/commands/package.rs index b01c5335..a064b034 100644 --- a/mcp/src/commands/package.rs +++ b/mcp/src/commands/package.rs @@ -41,8 +41,12 @@ const PACKAGE_TARGETS: &[PackageTarget] = &[ pub fn execute(args: &AssembleNpmArgs) -> ExitCode { match assemble_packages(args) { Ok(copied) => { + // Use stderr: this binary's primary mode is the MCP stdio server, + // and stdout is reserved for JSON-RPC framing. Routing all + // assemble-npm chatter to stderr keeps stdout protocol-safe even + // if the subcommand is ever invoked from a wrapped context. for path in copied { - println!("Hydrated {}", path.display()); + eprintln!("Hydrated {}", path.display()); } ExitCode::SUCCESS }