From d854bd8ae52a6784b7303061853daa2c5a4f5bd0 Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Wed, 19 Aug 2026 10:10:37 +0700 Subject: [PATCH 1/5] Truncate APIs and support graphql with mermaid --- crates/codegraph-api/src/lib.rs | 6 +++--- crates/codegraph-api/src/tools.rs | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/codegraph-api/src/lib.rs b/crates/codegraph-api/src/lib.rs index d0bf675dc..e7a08502c 100644 --- a/crates/codegraph-api/src/lib.rs +++ b/crates/codegraph-api/src/lib.rs @@ -15,15 +15,15 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +/// Transport-agnostic tool implementations (sandbox / diff / simulate) — dùng +/// chung bởi MCP server và GraphQL server. +pub mod tools; /// Mermaid diagram generators (control-flow / call-graph) — shared bởi mọi frontend. pub mod mermaid; /// Session — quản lý vòng đời index (bind/init/deindex/reindex) của một /// workspace root. Dùng chung bởi MCP server và GraphQL server (cả hai đều /// là transport mỏng trên tầng `codegraph-api`). pub mod session; -/// Transport-agnostic tool implementations (sandbox / diff / simulate) — dùng -/// chung bởi MCP server và GraphQL server. -pub mod tools; pub struct GraphApi { shared_index: Arc, diff --git a/crates/codegraph-api/src/tools.rs b/crates/codegraph-api/src/tools.rs index bf9095749..f67864248 100644 --- a/crates/codegraph-api/src/tools.rs +++ b/crates/codegraph-api/src/tools.rs @@ -208,7 +208,8 @@ pub async fn dispatch_sandbox( ) .await? .page; - hits.into_iter() + hits + .into_iter() .find(|s| matches!(s.kind, SymbolKind::Function | SymbolKind::Method)) .map(|s| s.id) .ok_or_else(|| Error::Invalid(format!("no function matching `{q}`")))? From 0c29aae580d0577839aa1a0e9857c87722739f5e Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Wed, 19 Aug 2026 21:28:00 +0700 Subject: [PATCH 2/5] Implement to support windows --- .github/workflows/ci.yml | 34 +++++ Cargo.lock | 1 + README.md | 1 + crates/codegraph-api/src/session.rs | 10 +- crates/codegraph-graph/Cargo.toml | 1 + crates/codegraph-graph/src/embeddings.rs | 10 +- crates/codegraph-graphql/src/lib.rs | 19 +++ crates/codegraph-graphql/src/query.rs | 41 ++++++ crates/codegraph-graphql/src/types.rs | 14 ++ crates/codegraph-mcp/src/http.rs | 3 +- crates/codegraph-mcp/src/lib.rs | 35 ++++- .../codegraph-mcp/src/server-instructions.md | 1 + crates/codegraph-mcp/src/tools.rs | 43 ++++++ crates/codegraph/src/main.rs | 131 ++++++++++++++++-- packaging/choco/codegraph.nuspec | 18 +++ packaging/choco/tools/chocolateyinstall.ps1 | 10 ++ packaging/winget/codegraph.yaml | 25 ++++ scripts/install.ps1 | 88 +++++++++--- 18 files changed, 442 insertions(+), 43 deletions(-) create mode 100644 packaging/choco/codegraph.nuspec create mode 100644 packaging/choco/tools/chocolateyinstall.ps1 create mode 100644 packaging/winget/codegraph.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88c57e1ec..11d5ab945 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,40 @@ jobs: - run: cargo clippy --workspace --all-targets -- -D warnings - run: cargo clippy -p codegraph-graph --features postgres,mysql,redis --tests -- -D warnings + clippy-windows: + name: clippy (windows) + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + targets: x86_64-pc-windows-msvc + - uses: Swatinem/rust-cache@v2 + - run: cargo clippy -p codegraph --target x86_64-pc-windows-msvc -- -D warnings + + test-windows: + name: test (windows) + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-pc-windows-msvc + - uses: Swatinem/rust-cache@v2 + - name: Build + run: cargo build -p codegraph + - name: Build (fastembed feature) + run: cargo build -p codegraph --features fastembed + - name: Test (binary crate) + run: cargo test -p codegraph + - name: Test (graph crate, sqlite/lmdb backends) + run: cargo test -p codegraph-graph --features sqlite,lmdb,bloom-search + - name: Smoke (doctor + help) + run: | + cargo run -p codegraph -- --help + cargo run -p codegraph -- doctor + test: name: test (${{ matrix.os }}) runs-on: ${{ matrix.os }} diff --git a/Cargo.lock b/Cargo.lock index 8e65504b0..2ca701f71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -830,6 +830,7 @@ dependencies = [ "codegraph-extract", "criterion", "dashmap", + "dirs 5.0.1", "fastembed", "libsqlite3-sys", "lmdb-rkv", diff --git a/README.md b/README.md index 823b98d75..ac9b1c59e 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,7 @@ report, plus the session tools `codegraph_init` / `codegraph_deinit` / | `codegraph_index` | Full re-index of the bound workspace | | `codegraph_sandbox` | Compile a function group to machine code and run it against Rhai mocks | | `codegraph_diff` | Draft report of what an MR/patch would change in the graph | +| `codegraph_mermaid` | Render a Mermaid diagram (flow / callers / callees / impact) — the visual variant of the diagram queries; requires the server to start with `--mermaid` | Read the [server instructions](crates/codegraph-mcp/src/server-instructions.md) that ship with the binary — they tell your agent when to reach for which tool. diff --git a/crates/codegraph-api/src/session.rs b/crates/codegraph-api/src/session.rs index c1cef96ca..44e24865f 100644 --- a/crates/codegraph-api/src/session.rs +++ b/crates/codegraph-api/src/session.rs @@ -322,15 +322,21 @@ fn normalize_root(path: Utf8PathBuf) -> Result { .map_err(|e| anyhow!("cannot resolve {}: {e}", path))?; let canon = Utf8PathBuf::from_path_buf(canon).map_err(|p| anyhow!("path is not valid UTF-8: {p:?}"))?; - if canon.as_str() == "/" { + if is_fs_root(&canon) { return Err(anyhow!( - "refusing to use `/` as the workspace root \ + "refusing to use the filesystem root as the workspace root \ (MCP hosts may launch servers from `/`). Pass an absolute project path." )); } Ok(canon) } +/// Đường dẫn có phải là gốc filesystem không (`/` trên Unix, `C:\` trên +/// Windows). Dùng `parent().is_none()` cho cross-platform (không so chuỗi `/`). +fn is_fs_root(p: &Utf8Path) -> bool { + p.parent().is_none() +} + /// Full re-index: mở index theo backend config → `Orchestrator::index_all` /// (ingest = full re-index, bump version → snapshot cũ bị `ensure_fresh` thấy /// stale và rebuild ở lần query kế). diff --git a/crates/codegraph-graph/Cargo.toml b/crates/codegraph-graph/Cargo.toml index 9b09bc87a..5cc09acd8 100644 --- a/crates/codegraph-graph/Cargo.toml +++ b/crates/codegraph-graph/Cargo.toml @@ -10,6 +10,7 @@ warnings = "deny" [dependencies] codegraph-core = { path = "../codegraph-core" } +dirs = { workspace = true } # SQLite-backed Db (moved here from the removed codegraph-db crate). rusqlite = { workspace = true } diff --git a/crates/codegraph-graph/src/embeddings.rs b/crates/codegraph-graph/src/embeddings.rs index fcb5e521b..24c459685 100644 --- a/crates/codegraph-graph/src/embeddings.rs +++ b/crates/codegraph-graph/src/embeddings.rs @@ -186,16 +186,16 @@ fn default_cache_dir() -> Option { expand_tilde("~/.cache/codegraph/embeddings") } -/// Expand `~` thành home dir (best-effort). Trả `Some` nếu không bắt đầu bằng `~`. +/// Expand `~` thành home dir (best-effort, cross-platform). Trả `Some` nếu +/// không bắt đầu bằng `~`. Dùng `dirs::home_dir()` để lấy home đúng trên mọi OS +/// (Windows: `USERPROFILE`/`HOMEDRIVE`, macOS/Linux: `$HOME`). fn expand_tilde(path: &str) -> Option { if !path.starts_with('~') { return Some(PathBuf::from(path)); } - let home = std::env::var("HOME") - .ok() - .or_else(|| std::env::var("USERPROFILE").ok())?; + let home = dirs::home_dir()?; let rest = path.strip_prefix('~').unwrap_or(""); - Some(PathBuf::from(home).join(rest.trim_start_matches('/'))) + Some(home.join(rest.trim_start_matches('/'))) } /// Suffix file extension của sqlite-vss theo OS (`.dylib` / `.so` / `.dll`). diff --git a/crates/codegraph-graphql/src/lib.rs b/crates/codegraph-graphql/src/lib.rs index bf049aff3..dcd5cc5ea 100644 --- a/crates/codegraph-graphql/src/lib.rs +++ b/crates/codegraph-graphql/src/lib.rs @@ -206,6 +206,25 @@ mod tests { assert_eq!(json["data"]["__typename"], "Query"); } + #[tokio::test] + async fn mermaid_gate_enforced() { + // Không bật --mermaid: mermaid phải báo lỗi gate (không gọi index). + let app = build_app(&cfg(false), make_state(false)); + let (status, json) = post_graphql(&app, r#"{ mermaid(id: "1", kind: FLOW) }"#).await; + assert_eq!(status, StatusCode::OK); + let msg = json["errors"][0]["message"].as_str().unwrap(); + assert!(msg.contains("Mermaid"), "expected gate error, got: {msg}"); + + // Bật --mermaid: vượt gate, sau đó lỗi do chưa có index (khác gate). + let app2 = build_app(&cfg(true), make_state(true)); + let (_status, json2) = post_graphql(&app2, r#"{ mermaid(id: "1", kind: FLOW) }"#).await; + let msg2 = json2["errors"][0]["message"].as_str().unwrap(); + assert!( + !msg2.contains("Mermaid"), + "gate should be off when --mermaid set, got: {msg2}" + ); + } + #[tokio::test] async fn api_key_required_when_set() { let mut c = cfg(false); diff --git a/crates/codegraph-graphql/src/query.rs b/crates/codegraph-graphql/src/query.rs index 453b4c3e2..5c631abbb 100644 --- a/crates/codegraph-graphql/src/query.rs +++ b/crates/codegraph-graphql/src/query.rs @@ -159,6 +159,47 @@ impl Query { .map_err(|e| async_graphql::Error::new(e.to_string())) } + /// Diagram Mermaid cho một symbol — biến thể hình ảnh của `flow` / + /// `callers` / `callees` / `impact`. `kind` chọn loại diagram; `depth` (mặc + /// định 1) giới hạn BFS hop cho callers/callees/impact (bị bỏ qua với flow). + /// Chỉ hoạt động khi server bật `--mermaid`; tắt → lỗi rõ ràng. + async fn mermaid( + &self, + ctx: &Context<'_>, + id: ID, + kind: MermaidKind, + depth: Option, + ) -> GqlResult { + let state = ctx.data::>()?; + if !state.mermaid { + return Err(async_graphql::Error::new( + "Mermaid output is disabled. Start the GraphQL server with --mermaid to enable diagram rendering.", + )); + } + let id = parse_id(&id)?; + let depth = depth.unwrap_or(1).max(1) as u32; + let api = api_for(ctx).await?; + let diagram = match kind { + MermaidKind::Flow => { + let flow = api + .flow(id) + .await + .map_err(|e| async_graphql::Error::new(e.to_string()))?; + codegraph_api::mermaid::control_flow(&flow) + } + MermaidKind::Callers => codegraph_api::mermaid::callers_mermaid(&api, id, depth) + .await + .map_err(|e| async_graphql::Error::new(e.to_string()))?, + MermaidKind::Callees => codegraph_api::mermaid::callees_mermaid(&api, id, depth) + .await + .map_err(|e| async_graphql::Error::new(e.to_string()))?, + MermaidKind::Impact => codegraph_api::mermaid::impact_mermaid(&api, id, depth) + .await + .map_err(|e| async_graphql::Error::new(e.to_string()))?, + }; + Ok(diagram) + } + /// Functions có chain chứa pattern (id/marker/tên symbol, cách nhau bởi `,`). async fn search_flow( &self, diff --git a/crates/codegraph-graphql/src/types.rs b/crates/codegraph-graphql/src/types.rs index 22ec336c8..dfbdd3a43 100644 --- a/crates/codegraph-graphql/src/types.rs +++ b/crates/codegraph-graphql/src/types.rs @@ -120,3 +120,17 @@ pub enum TypeKind { Interface, Enum, } + +// ==================== Mermaid kind ==================== + +/// Loại diagram Mermaid cho resolver `mermaid(id, kind, depth)` — render biến +/// thể hình ảnh của các query diagram (`flow` / `callers` / `callees` / `impact`). +/// Chỉ hoạt động khi server bật `--mermaid`. +#[derive(Enum, Copy, Clone, Eq, PartialEq, Debug)] +#[graphql(rename_items = "SCREAMING_SNAKE_CASE")] +pub enum MermaidKind { + Flow, + Callers, + Callees, + Impact, +} diff --git a/crates/codegraph-mcp/src/http.rs b/crates/codegraph-mcp/src/http.rs index 26ee37fa8..c5bde068f 100644 --- a/crates/codegraph-mcp/src/http.rs +++ b/crates/codegraph-mcp/src/http.rs @@ -41,6 +41,7 @@ use crate::{CodegraphServer, OutputStyle}; /// Không có — bind thất bại / lỗi serve trả `Err` qua `anyhow`. pub async fn serve_http( format: OutputStyle, + mermaid: bool, addr: SocketAddr, allowed_hosts: Vec, _enable_observability: bool, @@ -54,7 +55,7 @@ pub async fn serve_http( // Per SEP-2567 request 2026-07-28 vẫn luôn chạy stateless. .with_legacy_session_mode(true); let service = StreamableHttpService::new( - move || Ok(CodegraphServer::new_with_format(format)), + move || Ok(CodegraphServer::new_with_format(format, mermaid)), session_manager, config, ); diff --git a/crates/codegraph-mcp/src/lib.rs b/crates/codegraph-mcp/src/lib.rs index 3b2a5c4b6..9242a1cac 100644 --- a/crates/codegraph-mcp/src/lib.rs +++ b/crates/codegraph-mcp/src/lib.rs @@ -47,42 +47,54 @@ pub struct CodegraphServer { /// Session store cho search resumable — sống qua nhiều tool call để resume /// id (trả về khi timeout) có thể retry được. search_sessions: Arc, + /// Bật output Mermaid cho `codegraph_mermaid` (diagram visualization). Tắt → + /// tool trả lỗi rõ ràng. Tương ứng flag `--mermaid` ở CLI. + mermaid: bool, } impl CodegraphServer { /// Server với session trống — `codegraph_init` sẽ bind root trong phiên. pub fn new() -> Self { - Self::new_with_format(OutputStyle::default()) + Self::new_with_format(OutputStyle::default(), false) } /// `new()` nhưng seed output format từ CLI lúc khởi động - /// (`codegraph serve --mcp --format=...`). - pub fn new_with_format(format: OutputStyle) -> Self { + /// (`codegraph serve --mcp --format=...`), và flag `--mermaid`. + pub fn new_with_format(format: OutputStyle, mermaid: bool) -> Self { Self { session: Session::new_with_format(format), usage: Arc::new(Mutex::new(usage::UsageStats::default())), search_sessions: Arc::new(SearchSessionStore::new()), + mermaid, } } /// Pre-seed root từ `--path` lúc khởi động (tương đương đã `codegraph_init` /// với root đó, không index thêm). Giữ CLI/watcher flow không vỡ. pub async fn with_root(root: camino::Utf8PathBuf) -> anyhow::Result { - Self::with_root_and_format(root, OutputStyle::default()).await + Self::with_root_and_format(root, OutputStyle::default(), false).await } - /// `with_root()` nhưng seed output format từ CLI lúc khởi động. + /// `with_root()` nhưng seed output format và flag `--mermaid` từ CLI lúc + /// khởi động. pub async fn with_root_and_format( root: camino::Utf8PathBuf, format: OutputStyle, + mermaid: bool, ) -> anyhow::Result { Ok(Self { session: Session::with_root_and_format(root, format).await?, usage: Arc::new(Mutex::new(usage::UsageStats::default())), search_sessions: Arc::new(SearchSessionStore::new()), + mermaid, }) } + /// Flag `--mermaid` của server (gate cho `codegraph_mermaid`). + pub fn mermaid_enabled(&self) -> bool { + self.mermaid + } + /// Dispatch một tool call đã verify tên. Trả [`ToolOutput::Text`] cho thành /// công, [`ToolOutput::Error`] cho lỗi tool (client thấy `is_error`), /// [`Err`] cho lỗi protocol (unknown tool đã bị chặn trước ở `call_tool`). @@ -213,7 +225,18 @@ impl CodegraphServer { codegraph_api::tools::dispatch_origin_simulate(&root, sgi.clone(), args.clone()) .await } - _ => tools::dispatch_with_api(&api, &root, detail, format, name, args).await, + _ => { + tools::dispatch_with_api( + &api, + &root, + detail, + format, + self.mermaid_enabled(), + name, + args, + ) + .await + } }; match dispatch { diff --git a/crates/codegraph-mcp/src/server-instructions.md b/crates/codegraph-mcp/src/server-instructions.md index 342cfca00..482ead1ac 100644 --- a/crates/codegraph-mcp/src/server-instructions.md +++ b/crates/codegraph-mcp/src/server-instructions.md @@ -29,6 +29,7 @@ file-reading subtask — codegraph IS the index. | what does this call directly? | `codegraph_callees` | | change-impact radius | `codegraph_impact` | | call chain (markers + callees + sites) | `codegraph_flow` | +| diagram (Mermaid) of flow/callers/callees/impact | `codegraph_mermaid` (needs `--mermaid`) | | functions whose chain matches a pattern | `codegraph_search_flow` | | composed context for a symbol/topic | `codegraph_context` | | who calls library call `foo`? | `codegraph_references` | diff --git a/crates/codegraph-mcp/src/tools.rs b/crates/codegraph-mcp/src/tools.rs index 685fcd0ff..4721c4514 100644 --- a/crates/codegraph-mcp/src/tools.rs +++ b/crates/codegraph-mcp/src/tools.rs @@ -82,6 +82,15 @@ fn tool_defs() -> Vec { "format": { "type": "string", "enum": ["minimize", "medium"], "description": "Output format for this call (overrides session default): minimize = symbol items as fixed-order positional arrays (default), medium = objects with default-valued fields omitted." } }, "required": ["node"] }), ), + tool( + "codegraph_mermaid", + "Render a Mermaid diagram (flowchart / graph) for a symbol — the visual variant of the diagram queries. `kind`: 'flow' (control-flow chain), 'callers' (upstream, transitive), 'callees' (downstream), or 'impact' (transitive callers). `depth` limits BFS hops for callers/callees/impact (default 1; ignored for flow). Requires the server to start with --mermaid; otherwise the tool returns an error.", + json!({ "type": "object", "properties": { + "node": { "type": "integer", "description": "Symbol id to render." }, + "kind": { "type": "string", "enum": ["flow", "callers", "callees", "impact"], "default": "flow" }, + "depth": { "type": "integer", "default": 1, "description": "BFS hops for callers/callees/impact (ignored for flow)." } + }, "required": ["node"] }), + ), tool( "codegraph_search_flow", "Find functions whose call chain contains a pattern. Pattern = comma-separated tokens: numeric ids, marker names (LOOP, IF_TRUE, IF_FALSE, BRANCH_END, RETURN, LOOP_BACK, SWITCH_CASE, SWITCH_END, BREAK, CONTINUE, THROW) or symbol names. On large indexes pass timeout_ms (default 20000); if the call returns a timeout error containing \"resume\": \"\", retry the SAME call with that resume id to continue.", @@ -275,10 +284,44 @@ pub async fn dispatch_with_api( root: &Utf8Path, session_detail: DetailLevel, session_format: OutputStyle, + mermaid: bool, name: &str, args: Value, ) -> Result { match name { + "codegraph_mermaid" => { + if !mermaid { + return Err(Error::Invalid( + "Mermaid output is disabled. Start the MCP server with --mermaid to enable diagram rendering.".into(), + )); + } + let node = args.get("node").and_then(|v| v.as_u64()).ok_or_else(|| { + Error::Invalid("codegraph_mermaid requires `node` (symbol id)".into()) + })?; + let kind_str = args.get("kind").and_then(|v| v.as_str()).unwrap_or("flow"); + let depth = args + .get("depth") + .and_then(|v| v.as_u64()) + .unwrap_or(1) + .max(1) as u32; + let diagram = match kind_str { + "callers" => codegraph_api::mermaid::callers_mermaid(api, node, depth).await, + "callees" => codegraph_api::mermaid::callees_mermaid(api, node, depth).await, + "impact" => codegraph_api::mermaid::impact_mermaid(api, node, depth).await, + _ => { + let flow = api + .flow(node) + .await + .map_err(|e| Error::Invalid(e.to_string()))?; + Ok(codegraph_api::mermaid::control_flow(&flow)) + } + } + .map_err(|e| Error::Invalid(e.to_string()))?; + emit_value( + root.as_str(), + json!({ "node": node, "kind": kind_str, "mermaid": diagram }), + ) + } "codegraph_symbol" => { let detail = detail_from_args(&args, session_detail); let format = format_from_args(&args, session_format); diff --git a/crates/codegraph/src/main.rs b/crates/codegraph/src/main.rs index 74ddad13a..cac921acd 100644 --- a/crates/codegraph/src/main.rs +++ b/crates/codegraph/src/main.rs @@ -2,7 +2,7 @@ use anyhow::{anyhow, Result}; use camino::{Utf8Path, Utf8PathBuf}; use clap::{ArgAction, Parser, Subcommand}; use codegraph_extract::{ExtractStats, Orchestrator}; -use codegraph_graph::GraphIndex; +use codegraph_graph::{GraphIndex, SharedGraphIndex}; use codegraph_mcp::CodegraphServer; #[cfg(feature = "fastembed")] @@ -49,6 +49,9 @@ enum Cmd { }, /// Remove the .codegraph/ directory. Deinit, + /// Diagnose the environment: OS, codegraph version, whether the workspace is + /// initialized, index stats, and external tools (git/tar) on PATH. + Doctor, /// Pre-download an embedding model into the global cache (so semantic search /// works offline). Model is cached under `[embedding].cache_dir` (default /// `~/.cache/codegraph/embeddings`). Requires the `fastembed` feature. @@ -70,8 +73,9 @@ enum Cmd { /// động `init` workspace root; `--path` pre-bind nếu đã có `.codegraph/`. #[arg(long)] graphql: bool, - /// Bật Mermaid diagram output cho GraphQL API (`*_meraid`). Tắt → những - /// resolver này trả lỗi rõ ràng. Chỉ có nghĩa khi chạy `--graphql`. + /// Bật Mermaid diagram output (`codegraph_mermaid` ở MCP, `mermaid` ở + /// GraphQL). Tắt → những entry này trả lỗi rõ ràng. Có nghĩa cho cả + /// `--graphql` và `--mcp`/`--http`. #[arg(long)] mermaid: bool, /// Serve qua Streamable HTTP (POST/GET/DELETE + SSE) thay vì stdio — @@ -150,6 +154,7 @@ async fn main() -> Result<()> { match cmd { Cmd::Init { no_index, progress } => cmd_init(&root, !no_index, progress).await, Cmd::Deinit => cmd_deinit(&root), + Cmd::Doctor => cmd_doctor(&root).await, #[cfg(feature = "fastembed")] Cmd::Embed { model, cache_dir } => cmd_embed(&model, cache_dir.as_deref()).await, Cmd::Serve { @@ -189,6 +194,13 @@ fn is_initialized(root: &Utf8Path) -> bool { codegraph_extract::project_dir(root).exists() } +/// Đường dẫn có phải là gốc filesystem không (`/` trên Unix, `C:\` trên +/// Windows). Dùng để tránh bind workspace nhầm vào gốc ổ đĩa (MCP host thường +/// launch server từ `/`). Hoạt động cross-platform (không so sánh chuỗi `/`). +fn is_fs_root(p: &Utf8Path) -> bool { + p.parent().is_none() +} + /// Không có subcommand → in help. Banner console cũ bị bỏ: giao diện chính giờ /// là MCP (agent dùng `codegraph_init`/`codegraph_status` qua tools). async fn cmd_default(_root: &Utf8Path) -> Result<()> { @@ -266,6 +278,108 @@ fn cmd_deinit(root: &Utf8Path) -> Result<()> { Ok(()) } +/// `codegraph doctor`: kiểm tra môi trường cơ bản và in báo cáo human-readable +/// với status `[OK]` / `[WARN]` / `[FAIL]`. Exit code ≠ 0 nếu có bất kỳ `[FAIL]`. +async fn cmd_doctor(root: &Utf8Path) -> Result<()> { + let mut ok = 0u32; + let mut warn = 0u32; + let mut fail = 0u32; + + // 1. Binary / version — luôn OK (đang chạy). + println!( + "[OK] codegraph {} ({} / {})", + env!("CARGO_PKG_VERSION"), + std::env::consts::OS, + std::env::consts::ARCH + ); + ok += 1; + + // 2. Workspace root. + println!("[OK] workspace: {root}"); + ok += 1; + + // 3. Đã init chưa (thư mục `.codegraph/` tồn tại). + let initialized = is_initialized(root); + if initialized { + println!("[OK] initialized: .codegraph/ present"); + ok += 1; + } else { + println!("[WARN] not initialized: run `codegraph init`"); + warn += 1; + } + + // 4. Index stats (chỉ khi đã init) — đọc `sg_stats` từ đĩa O(1). + if initialized { + match codegraph_extract::ExtractConfig::load(root).storage_route(root) { + Some(route) => match SharedGraphIndex::open_route(Some(route)).await { + Ok(idx) => match idx.stats_cached().await { + Some(s) => { + println!( + "[OK] index: {} symbols, {} chains, {} edges, {} files", + s.symbols, s.chains, s.edges, s.files + ); + ok += 1; + } + None => { + println!("[WARN] index empty: run `codegraph init`"); + warn += 1; + } + }, + Err(e) => { + println!("[FAIL] cannot open index: {e}"); + fail += 1; + } + }, + // Backend in-memory: không có index local để inspect. + None => { + println!("[OK] index: in-memory backend (no local index to inspect)"); + ok += 1; + } + } + } + + // 5. External tools: git & tar (Windows: Git for Windows + tar.exe tích hợp). + for tool in ["git", "tar"] { + match check_tool_version(tool) { + Some(v) => { + println!("[OK] {tool}: {v}"); + ok += 1; + } + None => { + println!("[WARN] {tool} not found on PATH (needed for codegraph_diff_simulate)"); + warn += 1; + } + } + } + + println!("---"); + println!("{ok} OK, {warn} WARN, {fail} FAIL"); + if fail > 0 { + std::process::exit(1); + } + Ok(()) +} + +/// Trả version string của external tool nếu chạy được `--version`, ngược lại +/// `None` (tool không có trên PATH hoặc thoát lỗi). +fn check_tool_version(tool: &str) -> Option { + let out = std::process::Command::new(tool).arg("--version").output().ok()?; + if !out.status.success() { + return None; + } + let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string(); + if stdout.is_empty() { + // Một số bản tool in version ra stderr. + let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string(); + if stderr.is_empty() { + return Some("(present)".to_string()); + } + Some(stderr) + } else { + Some(stdout) + } +} + /// `codegraph embed --model `: pre-download model vào global cache để /// semantic search chạy offline. #[cfg(feature = "fastembed")] @@ -309,7 +423,7 @@ async fn cmd_serve( } else { Some(api_key.join(",")) }; - let use_root = root.as_str() != "/"; + let use_root = !is_fs_root(&root); let cfg = codegraph_graphql::ServeConfig { addr, api_key, @@ -339,12 +453,13 @@ async fn cmd_serve( } else { allowed_hosts.extend(allow_host); } - let use_root = root.as_str() != "/"; + let use_root = !is_fs_root(&root); if use_root && is_initialized(root) { watcher::spawn(root.to_path_buf(), storage_dsn(root)); } return codegraph_mcp::serve_http( format, + mermaid, addr, allowed_hosts, enable_observability, @@ -364,16 +479,16 @@ async fn cmd_serve( // like Claude Desktop launch servers with cwd=/ and no `--path` — the root // resolving to `/` is NOT an error anymore: we just start with an EMPTY // session and let the agent bind the project path through the tool. - let use_root = root.as_str() != "/"; + let use_root = !is_fs_root(&root); let initialized = use_root && is_initialized(root); let dsn = if initialized { storage_dsn(root) } else { None }; if initialized { watcher::spawn(root.to_path_buf(), dsn.clone()); } let server = if use_root { - CodegraphServer::with_root_and_format(root.to_path_buf(), format).await? + CodegraphServer::with_root_and_format(root.to_path_buf(), format, mermaid).await? } else { - CodegraphServer::new_with_format(format) + CodegraphServer::new_with_format(format, mermaid) }; codegraph_mcp::serve_stdio(server).await } diff --git a/packaging/choco/codegraph.nuspec b/packaging/choco/codegraph.nuspec new file mode 100644 index 000000000..a6cde89b7 --- /dev/null +++ b/packaging/choco/codegraph.nuspec @@ -0,0 +1,18 @@ + + + + codegraph + 1.2.0 + codegraph + Cleboost + https://github.com/Cleboost/codegraph-rs + https://github.com/Cleboost/codegraph-rs/blob/main/LICENSE + false + Local-first code intelligence: tree-sitter knowledge graph + MCP server. Indexes a codebase locally and exposes it over an MCP server and GraphQL API. + Local-first code intelligence (MCP server). + codegraph mcp code-intelligence tree-sitter graph + + + + + diff --git a/packaging/choco/tools/chocolateyinstall.ps1 b/packaging/choco/tools/chocolateyinstall.ps1 new file mode 100644 index 000000000..106f379d0 --- /dev/null +++ b/packaging/choco/tools/chocolateyinstall.ps1 @@ -0,0 +1,10 @@ +$ErrorActionPreference = 'Stop' + +$toolsDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$version = $env:ChocolateyPackageVersion +$url = "https://github.com/Cleboost/codegraph-rs/releases/download/v$version/codegraph-x86_64-pc-windows-msvc.zip" +$zip = Join-Path $toolsDir "codegraph-$version.zip" + +Get-ChocolateyWebFile -PackageName 'codegraph' -FileFullPath $zip -Url $url +Get-ChocolateyUnzip -FileFullPath $zip -Destination $toolsDir +Remove-Item -Force $zip diff --git a/packaging/winget/codegraph.yaml b/packaging/winget/codegraph.yaml new file mode 100644 index 000000000..d9337c50e --- /dev/null +++ b/packaging/winget/codegraph.yaml @@ -0,0 +1,25 @@ +# Winget manifest for codegraph (single-file / singleton form). +# +# NOTE: `InstallerSha256` MUST be replaced with the real SHA-256 of the +# `codegraph-x86_64-pc-windows-msvc.zip` asset for the released version. +# The release workflow (`release.yml`) produces that zip; fill the hash at +# release time (or automate it in the release pipeline before submitting to +# microsoft/winget-pkgs). +PackageIdentifier: Cleboost.codegraph +PackageVersion: 1.2.0 +PackageName: codegraph +Publisher: Cleboost +PublisherUrl: https://github.com/Cleboost/codegraph-rs +License: MIT +LicenseUrl: https://github.com/Cleboost/codegraph-rs/blob/main/LICENSE +ShortDescription: Local-first code intelligence (tree-sitter knowledge graph + MCP server) +Description: codegraph indexes a codebase into a local-first knowledge graph and exposes it over an MCP server and GraphQL API. +PackageUrl: https://github.com/Cleboost/codegraph-rs +InstallerType: zip +Installers: + - Architecture: x64 + InstallerUrl: https://github.com/Cleboost/codegraph-rs/releases/download/v1.2.0/codegraph-x86_64-pc-windows-msvc.zip + InstallerSha256: 0000000000000000000000000000000000000000000000000000000000000000 + InstallerType: zip +ManifestType: singleton +ManifestVersion: 1.6.0 diff --git a/scripts/install.ps1 b/scripts/install.ps1 index b9be5b9d6..1cef88aee 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -1,34 +1,61 @@ # codegraph install script for Windows -# Usage: irm https://raw.githubusercontent.com/Cleboost/codegraph-rs/main/scripts/install.ps1 | iex +# +# Usage (latest release, one-liner): +# irm https://raw.githubusercontent.com/Cleboost/codegraph-rs/main/scripts/install.ps1 | iex +# +# Usage (pin a version / download the script first): +# irm https://raw.githubusercontent.com/Cleboost/codegraph-rs/main/scripts/install.ps1 -OutFile install.ps1 +# .\install.ps1 -Version 1.2.0 + +[CmdletBinding()] +param( + # Pin a specific version, e.g. "1.2.0". Empty = latest release. + [string]$Version +) $ErrorActionPreference = 'Stop' -$Repo = 'Cleboost/codegraph-rs' -$BinName = 'codegraph.exe' -$InstallDir = if ($env:CODEGRAPH_INSTALL_DIR) { $env:CODEGRAPH_INSTALL_DIR } ` - else { Join-Path $env:LOCALAPPDATA 'codegraph\bin' } +$Repo = 'Cleboost/codegraph-rs' +$BinName = 'codegraph.exe' +$Target = 'x86_64-pc-windows-msvc' +$AssetName = "codegraph-$Target.zip" -# Detect architecture +# Install dir: $CODEGRAPH_INSTALL_DIR or %LOCALAPPDATA%\codegraph\bin +$InstallDir = if ($env:CODEGRAPH_INSTALL_DIR) { + $env:CODEGRAPH_INSTALL_DIR +} else { + Join-Path $env:LOCALAPPDATA 'codegraph\bin' +} + +# Detect architecture (only x86_64 is supported on Windows for now). $arch = (Get-CimInstance Win32_Processor).AddressWidth if ($arch -ne 64) { Write-Error "Only x86_64 is supported on Windows." exit 1 } -$Target = 'x86_64-pc-windows-msvc' -# Fetch latest release tag -Write-Host "Fetching latest release..." -$release = Invoke-RestMethod "https://api.github.com/repos/$Repo/releases/latest" -$Tag = $release.tag_name -if (-not $Tag) { - Write-Error "Could not detect latest release tag." - exit 1 +# Resolve the release tag. +if ($Version) { + $Tag = if ($Version.StartsWith('v')) { $Version } else { "v$Version" } + # Verify the release exists before downloading. + $release = Invoke-RestMethod "https://api.github.com/repos/$Repo/releases/tags/$Tag" + if (-not $release) { + Write-Error "Release $Tag not found." + exit 1 + } +} else { + Write-Host "Fetching latest release..." + $release = Invoke-RestMethod "https://api.github.com/repos/$Repo/releases/latest" + $Tag = $release.tag_name + if (-not $Tag) { + Write-Error "Could not detect latest release tag." + exit 1 + } } -$AssetName = "codegraph-$Target.zip" $Url = "https://github.com/$Repo/releases/download/$Tag/$AssetName" -# Download +# Download into a temp dir. $TmpDir = Join-Path $env:TEMP "codegraph-install-$(Get-Random)" New-Item -ItemType Directory -Path $TmpDir | Out-Null $ZipPath = Join-Path $TmpDir $AssetName @@ -36,22 +63,41 @@ $ZipPath = Join-Path $TmpDir $AssetName Write-Host "Downloading $Url" Invoke-WebRequest -Uri $Url -OutFile $ZipPath -UseBasicParsing -# Extract +if (-not (Test-Path $ZipPath) -or ((Get-Item $ZipPath).Length -eq 0)) { + Remove-Item -Recurse -Force $TmpDir + Write-Error "Download failed: $AssetName is missing or empty. Check that release $Tag ships a Windows build." + exit 1 +} + +# Extract. Expand-Archive -Path $ZipPath -DestinationPath $TmpDir -Force -# Install +$BinSrc = Join-Path $TmpDir $BinName +if (-not (Test-Path $BinSrc)) { + Remove-Item -Recurse -Force $TmpDir + Write-Error "Asset $AssetName did not contain $BinName." + exit 1 +} + +# Install. if (-not (Test-Path $InstallDir)) { New-Item -ItemType Directory -Path $InstallDir | Out-Null } -$BinSrc = Join-Path $TmpDir $BinName Copy-Item -Path $BinSrc -Destination (Join-Path $InstallDir $BinName) -Force -# Cleanup +# Cleanup. Remove-Item -Recurse -Force $TmpDir +# Verify the binary runs. +$Installed = Join-Path $InstallDir $BinName +if (-not (Test-Path $Installed)) { + Write-Error "Installation failed: $Installed not found." + exit 1 +} + Write-Host "Installed codegraph $Tag to $InstallDir" -# Add to user PATH if not already present +# Add to user PATH if not already present. $UserPath = [Environment]::GetEnvironmentVariable('Path', 'User') if ($UserPath -notlike "*$InstallDir*") { [Environment]::SetEnvironmentVariable('Path', "$UserPath;$InstallDir", 'User') From 329b3e9a4ed277e572362c12272500232e4816fa Mon Sep 17 00:00:00 2001 From: hungpham10 <136320753+hungpham10@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:46:33 +0000 Subject: [PATCH 3/5] style: apply rustfmt --- crates/codegraph-api/src/lib.rs | 6 +++--- crates/codegraph-api/src/tools.rs | 3 +-- crates/codegraph/src/main.rs | 5 ++++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/codegraph-api/src/lib.rs b/crates/codegraph-api/src/lib.rs index e7a08502c..d0bf675dc 100644 --- a/crates/codegraph-api/src/lib.rs +++ b/crates/codegraph-api/src/lib.rs @@ -15,15 +15,15 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; -/// Transport-agnostic tool implementations (sandbox / diff / simulate) — dùng -/// chung bởi MCP server và GraphQL server. -pub mod tools; /// Mermaid diagram generators (control-flow / call-graph) — shared bởi mọi frontend. pub mod mermaid; /// Session — quản lý vòng đời index (bind/init/deindex/reindex) của một /// workspace root. Dùng chung bởi MCP server và GraphQL server (cả hai đều /// là transport mỏng trên tầng `codegraph-api`). pub mod session; +/// Transport-agnostic tool implementations (sandbox / diff / simulate) — dùng +/// chung bởi MCP server và GraphQL server. +pub mod tools; pub struct GraphApi { shared_index: Arc, diff --git a/crates/codegraph-api/src/tools.rs b/crates/codegraph-api/src/tools.rs index f67864248..bf9095749 100644 --- a/crates/codegraph-api/src/tools.rs +++ b/crates/codegraph-api/src/tools.rs @@ -208,8 +208,7 @@ pub async fn dispatch_sandbox( ) .await? .page; - hits - .into_iter() + hits.into_iter() .find(|s| matches!(s.kind, SymbolKind::Function | SymbolKind::Method)) .map(|s| s.id) .ok_or_else(|| Error::Invalid(format!("no function matching `{q}`")))? diff --git a/crates/codegraph/src/main.rs b/crates/codegraph/src/main.rs index cac921acd..425fac85e 100644 --- a/crates/codegraph/src/main.rs +++ b/crates/codegraph/src/main.rs @@ -363,7 +363,10 @@ async fn cmd_doctor(root: &Utf8Path) -> Result<()> { /// Trả version string của external tool nếu chạy được `--version`, ngược lại /// `None` (tool không có trên PATH hoặc thoát lỗi). fn check_tool_version(tool: &str) -> Option { - let out = std::process::Command::new(tool).arg("--version").output().ok()?; + let out = std::process::Command::new(tool) + .arg("--version") + .output() + .ok()?; if !out.status.success() { return None; } From 50fdb09f8da5670860a752583e28b901985fc6fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=E1=BA=A1m=20Nguy=E1=BB=85n=20Xu=C3=A2n=20H=C3=B9ng?= <136320753+hungpham10@users.noreply.github.com> Date: Wed, 19 Aug 2026 22:53:07 +0700 Subject: [PATCH 4/5] Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11d5ab945..b759e181b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,9 @@ on: pull_request: workflow_dispatch: +permissions: + contents: read + env: CARGO_TERM_COLOR: always RUSTFLAGS: -D warnings From 42972a3022fed3308de1a01062fcf9a94221c78f Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Wed, 19 Aug 2026 22:58:35 +0700 Subject: [PATCH 5/5] Fix lint --- crates/codegraph/src/main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/codegraph/src/main.rs b/crates/codegraph/src/main.rs index 425fac85e..97fd921d8 100644 --- a/crates/codegraph/src/main.rs +++ b/crates/codegraph/src/main.rs @@ -426,7 +426,7 @@ async fn cmd_serve( } else { Some(api_key.join(",")) }; - let use_root = !is_fs_root(&root); + let use_root = !is_fs_root(root); let cfg = codegraph_graphql::ServeConfig { addr, api_key, @@ -456,7 +456,7 @@ async fn cmd_serve( } else { allowed_hosts.extend(allow_host); } - let use_root = !is_fs_root(&root); + let use_root = !is_fs_root(root); if use_root && is_initialized(root) { watcher::spawn(root.to_path_buf(), storage_dsn(root)); } @@ -482,7 +482,7 @@ async fn cmd_serve( // like Claude Desktop launch servers with cwd=/ and no `--path` — the root // resolving to `/` is NOT an error anymore: we just start with an EMPTY // session and let the agent bind the project path through the tool. - let use_root = !is_fs_root(&root); + let use_root = !is_fs_root(root); let initialized = use_root && is_initialized(root); let dsn = if initialized { storage_dsn(root) } else { None }; if initialized {