From b693a905a13fe762eff8325b430ca6116dfad216 Mon Sep 17 00:00:00 2001 From: crsei Date: Tue, 21 Apr 2026 03:27:03 -0400 Subject: [PATCH] refactor(workspace): P2 extract cc-bootstrap, cc-auth, cc-skills leaf crates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three Phase-2 extractions in one commit; each builds + tests green at the boundary, and the root crate keeps every `crate::{auth,bootstrap, skills}::…` path working via `use cc_X as X;` aliases at the crate root (same pattern #79 used for `cc-keybindings` and `cc-observability`). cc-bootstrap — 7 files, 922 LOC (clean leaf) ------------------------------------------- `src/bootstrap/{diagnostics,ids,mod,model,signal,state,timing}.rs` → `crates/cc-bootstrap/src/…` with a pure `git mv`. No dependency-injection needed: `bootstrap/` was already a true import-DAG leaf per its own module doc. Direct deps: `serde`, `parking_lot`, `tokio`, `uuid` (+ `serde_json` for the round-trip test in `ids.rs`). cc-auth — 8 files, ~1,500 LOC (one tie broken) --------------------------------------------- `src/auth/{api_key,codex_cli,mod,token}.rs` and `src/auth/oauth/{mod,client,config,pkce}.rs` → `crates/cc-auth/src/…`. The one reverse-dep was `token.rs:9` calling `crate::config::paths::credentials_path()`. Broken by a global init registered from the host at process startup (mirrors the `set_event_sender` pattern already used by mcp/plugins/lsp/skills): - `cc_auth::set_credentials_path(PathBuf)` — host calls this at the top of `fn main()` with `crate::config::paths::credentials_path()`. - `token::token_file_path()` reads it back via a private `crate::credentials_path()` helper. - Falls back to `{CC_RUST_HOME | ~/.cc-rust | $TMP/cc-rust}/credentials.json` when the host hasn't registered one, so unit tests that exercise `resolve_auth()` directly (doctor, logout, voice_cmd — 10 tests) keep passing. The fallback duplicates ~10 LOC from `config::paths::data_root` — a small price for full decoupling. Keychain service name stays `"cc-rust"` (the original acceptance criterion on #71). OAuth scopes, PKCE, and token-refresh flow are untouched — only the storage path resolution changed. Direct deps: `anyhow`, `serde`, `serde_json`, `chrono`, `tokio`, `tracing`, `reqwest`, `keyring`, `base64`, `rand`, `sha2`, `dirs`, `parking_lot`, `urlencoding`. cc-skills — 3 files, ~1,000 LOC (two ties broken) ------------------------------------------------ `src/skills/{bundled,loader,mod}.rs` → `crates/cc-skills/src/…`. Two reverse-deps broken: 1. **Event emission** — `emit_event` held a `broadcast::Sender`, which is a cycle the moment skills leaves the root crate. Replaced with a minimal cc-skills-owned enum and a callback: ```rust pub enum SkillSubsystemEvent { SkillsLoaded { count: usize } } pub fn set_event_callback(cb: F); ``` The host adapts it into `SubsystemEvent::Skill(SkillEvent::SkillsLoaded { .. })` in `ipc/runtime.rs` (replaces the old `set_event_sender(bus.sender())` line). 2. **User-skills directory** — `init_skills` used to resolve `crate::config::paths::skills_dir_global()` internally. It now takes the directory as its first parameter; `main.rs` and `ipc::subsystem_handlers` pass `&crate::config::paths::skills_dir_global()` at the call site. Direct deps: `serde`, `parking_lot` only. Verification ------------ - `cargo build` : ok, 2 warnings (pre-existing `web::handlers::session_id` dead_code, identical on `rust-lite@ab8a2fc`). - `cargo build --release` : ok, same 2 warnings. - `cargo test --workspace --lib --offline --no-fail-fast -- --test-threads=1`: - `cc-auth` — 32 passed, 0 failed, 2 ignored - `cc-bootstrap` — 28 passed, 0 failed - `cc-keybindings` — 51 passed, 0 failed (unchanged from P1) - `cc-observability` — 10 passed, 1 pre-existing flake (`off_mode_skips_redaction`, tracked in #79) - `cc-skills` — 21 passed, 0 failed - `cc-types` — 0 tests - `cargo test --bin claude-code-rs --offline -- --test-threads=1` : **1713 / 1713 pass**. Baseline from PR #80 was 1794 — the 81-test drop is the tests that moved out with their code (cc-bootstrap 28 + cc-auth 32 + cc-skills 21 = 81, matches exactly). - Smoke: `claude-code-rs --version` prints `claude-code-rs 0.1.0`. Phase 2 checklist (#71) ----------------------- - [x] `crates/cc-bootstrap/` created - [x] `crates/cc-auth/` created - [x] `crates/cc-skills/` created - [x] `src/{bootstrap,auth,skills}/` removed from root crate - [x] Keychain service name unchanged (`"cc-rust"`) - [x] All tests pass (1713 bin + 142 extracted, one pre-existing cc-observability flake unchanged) - [ ] Manual OAuth login E2E (`/login 2` + `/login-code`) — not reproducible in CI, flagged for maintainer verification. Refs #71. Part of #68. --- Cargo.lock | 43 +++++++++++++ Cargo.toml | 3 + crates/cc-auth/Cargo.toml | 24 ++++++++ .../src/auth => cc-auth/src}/api_key.rs | 0 .../src/auth => cc-auth/src}/codex_cli.rs | 0 .../src/auth/mod.rs => cc-auth/src/lib.rs} | 50 ++++++++++++++- .../src/auth => cc-auth/src}/oauth/client.rs | 0 .../src/auth => cc-auth/src}/oauth/config.rs | 0 .../src/auth => cc-auth/src}/oauth/mod.rs | 0 .../src/auth => cc-auth/src}/oauth/pkce.rs | 0 .../src/auth => cc-auth/src}/token.rs | 12 +++- crates/cc-bootstrap/Cargo.toml | 14 +++++ .../src}/diagnostics.rs | 0 .../src/bootstrap => cc-bootstrap/src}/ids.rs | 0 .../mod.rs => cc-bootstrap/src/lib.rs} | 0 .../bootstrap => cc-bootstrap/src}/model.rs | 0 .../bootstrap => cc-bootstrap/src}/signal.rs | 0 .../bootstrap => cc-bootstrap/src}/state.rs | 0 .../bootstrap => cc-bootstrap/src}/timing.rs | 0 crates/cc-skills/Cargo.toml | 9 +++ .../src/skills => cc-skills/src}/bundled.rs | 11 ++-- .../skills/mod.rs => cc-skills/src/lib.rs} | 61 ++++++++++++------- .../src/skills => cc-skills/src}/loader.rs | 0 crates/claude-code-rs/Cargo.toml | 5 +- crates/claude-code-rs/src/ipc/runtime.rs | 14 ++++- .../src/ipc/subsystem_handlers.rs | 5 +- crates/claude-code-rs/src/main.rs | 26 +++++--- 27 files changed, 235 insertions(+), 42 deletions(-) create mode 100644 crates/cc-auth/Cargo.toml rename crates/{claude-code-rs/src/auth => cc-auth/src}/api_key.rs (100%) rename crates/{claude-code-rs/src/auth => cc-auth/src}/codex_cli.rs (100%) rename crates/{claude-code-rs/src/auth/mod.rs => cc-auth/src/lib.rs} (86%) rename crates/{claude-code-rs/src/auth => cc-auth/src}/oauth/client.rs (100%) rename crates/{claude-code-rs/src/auth => cc-auth/src}/oauth/config.rs (100%) rename crates/{claude-code-rs/src/auth => cc-auth/src}/oauth/mod.rs (100%) rename crates/{claude-code-rs/src/auth => cc-auth/src}/oauth/pkce.rs (100%) rename crates/{claude-code-rs/src/auth => cc-auth/src}/token.rs (86%) create mode 100644 crates/cc-bootstrap/Cargo.toml rename crates/{claude-code-rs/src/bootstrap => cc-bootstrap/src}/diagnostics.rs (100%) rename crates/{claude-code-rs/src/bootstrap => cc-bootstrap/src}/ids.rs (100%) rename crates/{claude-code-rs/src/bootstrap/mod.rs => cc-bootstrap/src/lib.rs} (100%) rename crates/{claude-code-rs/src/bootstrap => cc-bootstrap/src}/model.rs (100%) rename crates/{claude-code-rs/src/bootstrap => cc-bootstrap/src}/signal.rs (100%) rename crates/{claude-code-rs/src/bootstrap => cc-bootstrap/src}/state.rs (100%) rename crates/{claude-code-rs/src/bootstrap => cc-bootstrap/src}/timing.rs (100%) create mode 100644 crates/cc-skills/Cargo.toml rename crates/{claude-code-rs/src/skills => cc-skills/src}/bundled.rs (97%) rename crates/{claude-code-rs/src/skills/mod.rs => cc-skills/src/lib.rs} (85%) rename crates/{claude-code-rs/src/skills => cc-skills/src}/loader.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index b234052b..5a7adacc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -479,6 +479,38 @@ dependencies = [ "shlex", ] +[[package]] +name = "cc-auth" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64", + "chrono", + "dirs", + "keyring", + "parking_lot", + "rand 0.8.5", + "reqwest", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "tracing", + "urlencoding", +] + +[[package]] +name = "cc-bootstrap" +version = "0.1.0" +dependencies = [ + "parking_lot", + "serde", + "serde_json", + "tokio", + "uuid", +] + [[package]] name = "cc-keybindings" version = "0.1.0" @@ -504,6 +536,14 @@ dependencies = [ "uuid", ] +[[package]] +name = "cc-skills" +version = "0.1.0" +dependencies = [ + "parking_lot", + "serde", +] + [[package]] name = "cc-types" version = "0.1.0" @@ -592,8 +632,11 @@ dependencies = [ "axum", "base64", "bytes", + "cc-auth", + "cc-bootstrap", "cc-keybindings", "cc-observability", + "cc-skills", "cc-types", "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index e4dd6866..b3218de9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -123,6 +123,9 @@ parking_lot = "0.12" cc-keybindings = { path = "crates/cc-keybindings" } cc-observability = { path = "crates/cc-observability" } cc-types = { path = "crates/cc-types" } +cc-bootstrap = { path = "crates/cc-bootstrap" } +cc-auth = { path = "crates/cc-auth" } +cc-skills = { path = "crates/cc-skills" } # Daemon HTTP server axum = { version = "0.8", features = ["ws"] } diff --git a/crates/cc-auth/Cargo.toml b/crates/cc-auth/Cargo.toml new file mode 100644 index 00000000..2a45a29b --- /dev/null +++ b/crates/cc-auth/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "cc-auth" +version = "0.1.0" +edition = "2021" +description = "Authentication (API key, OAuth, Keychain) for cc-rust" + +[dependencies] +anyhow = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +chrono = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +reqwest = { workspace = true } +keyring = { workspace = true } +base64 = { workspace = true } +rand = { workspace = true } +sha2 = { workspace = true } +dirs = { workspace = true } +parking_lot = { workspace = true } +urlencoding = { workspace = true } + +[dev-dependencies] +tempfile = { workspace = true } diff --git a/crates/claude-code-rs/src/auth/api_key.rs b/crates/cc-auth/src/api_key.rs similarity index 100% rename from crates/claude-code-rs/src/auth/api_key.rs rename to crates/cc-auth/src/api_key.rs diff --git a/crates/claude-code-rs/src/auth/codex_cli.rs b/crates/cc-auth/src/codex_cli.rs similarity index 100% rename from crates/claude-code-rs/src/auth/codex_cli.rs rename to crates/cc-auth/src/codex_cli.rs diff --git a/crates/claude-code-rs/src/auth/mod.rs b/crates/cc-auth/src/lib.rs similarity index 86% rename from crates/claude-code-rs/src/auth/mod.rs rename to crates/cc-auth/src/lib.rs index 61a6b97a..9598cbb8 100644 --- a/crates/claude-code-rs/src/auth/mod.rs +++ b/crates/cc-auth/src/lib.rs @@ -3,7 +3,7 @@ //! Supports three active auth methods: //! - API Key: via `ANTHROPIC_API_KEY` env var or system keychain //! - External Auth Token: via `ANTHROPIC_AUTH_TOKEN` env var -//! - OAuth Token: from `~/.cc-rust/credentials.json` (Claude.ai / Console / OpenAI Codex) +//! - OAuth Token: from the registered credentials path (Claude.ai / Console / OpenAI Codex) pub mod api_key; pub mod codex_cli; @@ -12,6 +12,54 @@ pub mod token; const OPENAI_CODEX_AUTH_TOKEN_ENV: &str = "OPENAI_CODEX_AUTH_TOKEN"; +// --------------------------------------------------------------------------- +// Host-provided credentials path +// --------------------------------------------------------------------------- +// +// cc-auth used to call `crate::config::paths::credentials_path()` directly +// from `token.rs`. That's a cycle the moment `auth` moves out of the root +// crate, so the host now registers the path once at startup and cc-auth reads +// it back through this module. + +use parking_lot::RwLock; +use std::path::PathBuf; +use std::sync::LazyLock; + +static CREDENTIALS_PATH: LazyLock>> = + LazyLock::new(|| RwLock::new(None)); + +/// Register the OAuth credentials file path. The host calls this once during +/// process startup; if a caller reaches token I/O without it having run +/// (e.g. a unit test that exercises `resolve_auth` directly), the fallback +/// in [`credentials_path`] mirrors the root crate's +/// `config::paths::credentials_path()` layout. +pub fn set_credentials_path(path: PathBuf) { + *CREDENTIALS_PATH.write() = Some(path); +} + +/// Return the registered credentials path, falling back to +/// `{CC_RUST_HOME | ~/.cc-rust | $TMP/cc-rust}/credentials.json` when the host +/// hasn't registered one. Kept in sync with `config::paths::data_root` in the +/// root crate — a small duplication that decouples cc-auth from it. +pub(crate) fn credentials_path() -> PathBuf { + if let Some(p) = CREDENTIALS_PATH.read().clone() { + return p; + } + data_root_fallback().join("credentials.json") +} + +fn data_root_fallback() -> PathBuf { + if let Ok(override_dir) = std::env::var("CC_RUST_HOME") { + if !override_dir.trim().is_empty() { + return PathBuf::from(override_dir); + } + } + if let Some(home) = dirs::home_dir() { + return home.join(".cc-rust"); + } + std::env::temp_dir().join("cc-rust") +} + // --------------------------------------------------------------------------- // Auth method enum // --------------------------------------------------------------------------- diff --git a/crates/claude-code-rs/src/auth/oauth/client.rs b/crates/cc-auth/src/oauth/client.rs similarity index 100% rename from crates/claude-code-rs/src/auth/oauth/client.rs rename to crates/cc-auth/src/oauth/client.rs diff --git a/crates/claude-code-rs/src/auth/oauth/config.rs b/crates/cc-auth/src/oauth/config.rs similarity index 100% rename from crates/claude-code-rs/src/auth/oauth/config.rs rename to crates/cc-auth/src/oauth/config.rs diff --git a/crates/claude-code-rs/src/auth/oauth/mod.rs b/crates/cc-auth/src/oauth/mod.rs similarity index 100% rename from crates/claude-code-rs/src/auth/oauth/mod.rs rename to crates/cc-auth/src/oauth/mod.rs diff --git a/crates/claude-code-rs/src/auth/oauth/pkce.rs b/crates/cc-auth/src/oauth/pkce.rs similarity index 100% rename from crates/claude-code-rs/src/auth/oauth/pkce.rs rename to crates/cc-auth/src/oauth/pkce.rs diff --git a/crates/claude-code-rs/src/auth/token.rs b/crates/cc-auth/src/token.rs similarity index 86% rename from crates/claude-code-rs/src/auth/token.rs rename to crates/cc-auth/src/token.rs index e864625b..06721ea2 100644 --- a/crates/claude-code-rs/src/auth/token.rs +++ b/crates/cc-auth/src/token.rs @@ -1,12 +1,18 @@ //! OAuth token persistence. //! -//! Stores OAuth tokens at `~/.cc-rust/credentials.json`. +//! Stores OAuth tokens at the path registered via +//! [`crate::set_credentials_path`]. The path is injected from the root crate at +//! startup (see `main.rs`) so cc-auth stays decoupled from `config::paths`. use anyhow::Result; -/// Token storage file path: `{data_root}/credentials.json` +/// Token storage file path (set once by the host at startup). +/// +/// Panics if [`crate::set_credentials_path`] has not been called yet. Any code +/// path that reaches token I/O runs after the early bootstrap has registered +/// the path, so this is a programmer-error guard rather than a runtime check. pub fn token_file_path() -> std::path::PathBuf { - crate::config::paths::credentials_path() + crate::credentials_path() } /// Stored token data (OAuth). diff --git a/crates/cc-bootstrap/Cargo.toml b/crates/cc-bootstrap/Cargo.toml new file mode 100644 index 00000000..d44b89b2 --- /dev/null +++ b/crates/cc-bootstrap/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "cc-bootstrap" +version = "0.1.0" +edition = "2021" +description = "Process-level singleton layer (session IDs, model strings, timing, diagnostics) for cc-rust" + +[dependencies] +serde = { workspace = true } +parking_lot = { workspace = true } +tokio = { workspace = true } +uuid = { workspace = true } + +[dev-dependencies] +serde_json = { workspace = true } diff --git a/crates/claude-code-rs/src/bootstrap/diagnostics.rs b/crates/cc-bootstrap/src/diagnostics.rs similarity index 100% rename from crates/claude-code-rs/src/bootstrap/diagnostics.rs rename to crates/cc-bootstrap/src/diagnostics.rs diff --git a/crates/claude-code-rs/src/bootstrap/ids.rs b/crates/cc-bootstrap/src/ids.rs similarity index 100% rename from crates/claude-code-rs/src/bootstrap/ids.rs rename to crates/cc-bootstrap/src/ids.rs diff --git a/crates/claude-code-rs/src/bootstrap/mod.rs b/crates/cc-bootstrap/src/lib.rs similarity index 100% rename from crates/claude-code-rs/src/bootstrap/mod.rs rename to crates/cc-bootstrap/src/lib.rs diff --git a/crates/claude-code-rs/src/bootstrap/model.rs b/crates/cc-bootstrap/src/model.rs similarity index 100% rename from crates/claude-code-rs/src/bootstrap/model.rs rename to crates/cc-bootstrap/src/model.rs diff --git a/crates/claude-code-rs/src/bootstrap/signal.rs b/crates/cc-bootstrap/src/signal.rs similarity index 100% rename from crates/claude-code-rs/src/bootstrap/signal.rs rename to crates/cc-bootstrap/src/signal.rs diff --git a/crates/claude-code-rs/src/bootstrap/state.rs b/crates/cc-bootstrap/src/state.rs similarity index 100% rename from crates/claude-code-rs/src/bootstrap/state.rs rename to crates/cc-bootstrap/src/state.rs diff --git a/crates/claude-code-rs/src/bootstrap/timing.rs b/crates/cc-bootstrap/src/timing.rs similarity index 100% rename from crates/claude-code-rs/src/bootstrap/timing.rs rename to crates/cc-bootstrap/src/timing.rs diff --git a/crates/cc-skills/Cargo.toml b/crates/cc-skills/Cargo.toml new file mode 100644 index 00000000..8df210d5 --- /dev/null +++ b/crates/cc-skills/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "cc-skills" +version = "0.1.0" +edition = "2021" +description = "Skill discovery, loading, and registry for cc-rust (bundled + user + project + plugin)" + +[dependencies] +serde = { workspace = true } +parking_lot = { workspace = true } diff --git a/crates/claude-code-rs/src/skills/bundled.rs b/crates/cc-skills/src/bundled.rs similarity index 97% rename from crates/claude-code-rs/src/skills/bundled.rs rename to crates/cc-skills/src/bundled.rs index ca1f7a6c..b423f5e8 100644 --- a/crates/claude-code-rs/src/skills/bundled.rs +++ b/crates/cc-skills/src/bundled.rs @@ -166,15 +166,14 @@ pub fn register_bundled_skills() { #[cfg(test)] mod tests { use super::*; - use crate::skills; #[test] fn test_register_bundled_skills() { // Clear any previous state - skills::clear_skills(); + crate::clear_skills(); register_bundled_skills(); - let all = skills::get_all_skills(); + let all = crate::get_all_skills(); // We should have at least 5 bundled skills assert!( @@ -196,7 +195,7 @@ mod tests { // Ensure bundled skills are registered (may already be from other tests) register_bundled_skills(); - let all = skills::get_all_skills(); + let all = crate::get_all_skills(); // These may or may not be present due to concurrent clear_skills() from other tests. // We verify the properties of SkillDefinition directly instead. let simplify = SkillDefinition { @@ -236,10 +235,10 @@ mod tests { #[test] fn test_bundled_skill_prompts_not_empty() { - skills::clear_skills(); + crate::clear_skills(); register_bundled_skills(); - let all = skills::get_all_skills(); + let all = crate::get_all_skills(); for skill in &all { assert!( !skill.prompt_body.is_empty(), diff --git a/crates/claude-code-rs/src/skills/mod.rs b/crates/cc-skills/src/lib.rs similarity index 85% rename from crates/claude-code-rs/src/skills/mod.rs rename to crates/cc-skills/src/lib.rs index 696cb404..21fdaed0 100644 --- a/crates/claude-code-rs/src/skills/mod.rs +++ b/crates/cc-skills/src/lib.rs @@ -161,24 +161,37 @@ impl SkillDefinition { // --------------------------------------------------------------------------- // Subsystem event emission // --------------------------------------------------------------------------- +// +// cc-skills used to hold a `broadcast::Sender` +// directly. Once `skills` moved into its own crate (issue #71), referencing +// the root crate's `ipc` module would have been a cycle. The host now +// registers a simple callback that receives cc-skills's own minimal event +// enum and is responsible for adapting it into `SubsystemEvent`. + +/// Minimal event set emitted by the skill subsystem. The host adapts these +/// into its own subsystem-event wrapper. +#[derive(Debug, Clone)] +pub enum SkillSubsystemEvent { + /// Skills were loaded / reloaded. + SkillsLoaded { count: usize }, +} -/// Event sender for subsystem events. -static EVENT_TX: LazyLock< - Mutex>>, -> = LazyLock::new(|| Mutex::new(None)); +type EventCallback = Box; -/// Inject the event sender from the headless event loop. -#[allow(dead_code)] // Called by headless event loop wiring (Task 12). -pub fn set_event_sender( - tx: tokio::sync::broadcast::Sender, -) { - *EVENT_TX.lock() = Some(tx); +static EVENT_CALLBACK: LazyLock>> = LazyLock::new(|| Mutex::new(None)); + +/// Register the host's event adapter. Replaces any previous callback. +pub fn set_event_callback(cb: F) +where + F: Fn(SkillSubsystemEvent) + Send + Sync + 'static, +{ + *EVENT_CALLBACK.lock() = Some(Box::new(cb)); } -/// Emit a subsystem event. -fn emit_event(event: crate::ipc::subsystem_events::SubsystemEvent) { - if let Some(tx) = EVENT_TX.lock().as_ref() { - let _ = tx.send(event); +/// Emit an event through the registered callback (no-op if unset). +fn emit_event(event: SkillSubsystemEvent) { + if let Some(cb) = EVENT_CALLBACK.lock().as_ref() { + cb(event); } } @@ -226,14 +239,20 @@ pub fn clear_skills() { } /// Initialize the skill system — loads bundled + directory skills. -pub fn init_skills(project_dir: Option<&std::path::Path>) { +/// +/// `user_skills_dir` is the path that used to be resolved internally via +/// `crate::config::paths::skills_dir_global()`. The host passes it in so +/// cc-skills stays decoupled from the root crate's path layer. +pub fn init_skills( + user_skills_dir: &std::path::Path, + project_dir: Option<&std::path::Path>, +) { // 1. Register bundled skills bundled::register_bundled_skills(); - // 2. Load user skills from {data_root}/skills/ - let user_skills_dir = crate::config::paths::skills_dir_global(); + // 2. Load user skills from the host-provided directory if user_skills_dir.is_dir() { - let skills = loader::load_skills_from_dir(&user_skills_dir, SkillSource::User); + let skills = loader::load_skills_from_dir(user_skills_dir, SkillSource::User); for skill in skills { register_skill(skill); } @@ -250,11 +269,9 @@ pub fn init_skills(project_dir: Option<&std::path::Path>) { } } - // 4. Emit skills-loaded event + // 4. Emit skills-loaded event through the host-registered callback let count = get_all_skills().len(); - emit_event(crate::ipc::subsystem_events::SubsystemEvent::Skill( - crate::ipc::subsystem_events::SkillEvent::SkillsLoaded { count }, - )); + emit_event(SkillSubsystemEvent::SkillsLoaded { count }); } // --------------------------------------------------------------------------- diff --git a/crates/claude-code-rs/src/skills/loader.rs b/crates/cc-skills/src/loader.rs similarity index 100% rename from crates/claude-code-rs/src/skills/loader.rs rename to crates/cc-skills/src/loader.rs diff --git a/crates/claude-code-rs/Cargo.toml b/crates/claude-code-rs/Cargo.toml index f022887a..30211aa0 100644 --- a/crates/claude-code-rs/Cargo.toml +++ b/crates/claude-code-rs/Cargo.toml @@ -131,10 +131,13 @@ dotenvy = { workspace = true } # 同步原语 parking_lot = { workspace = true } -# 内部 workspace crates (P1) +# 内部 workspace crates (P1/P2) cc-keybindings = { workspace = true } cc-observability = { workspace = true } cc-types = { workspace = true } +cc-bootstrap = { workspace = true } +cc-auth = { workspace = true } +cc-skills = { workspace = true } # Daemon axum = { workspace = true } diff --git a/crates/claude-code-rs/src/ipc/runtime.rs b/crates/claude-code-rs/src/ipc/runtime.rs index f7b7a718..3e550aca 100644 --- a/crates/claude-code-rs/src/ipc/runtime.rs +++ b/crates/claude-code-rs/src/ipc/runtime.rs @@ -74,7 +74,19 @@ impl HeadlessRuntime { crate::lsp_service::set_event_sender(event_bus.sender()); crate::mcp::set_event_sender(event_bus.sender()); crate::plugins::set_event_sender(event_bus.sender()); - crate::skills::set_event_sender(event_bus.sender()); + // cc-skills lives in its own crate and no longer knows about + // `SubsystemEvent`. Adapt its minimal event enum into ours here. + let skills_tx = event_bus.sender(); + crate::skills::set_event_callback(move |e| { + let adapted = match e { + crate::skills::SkillSubsystemEvent::SkillsLoaded { count } => { + super::subsystem_events::SubsystemEvent::Skill( + super::subsystem_events::SkillEvent::SkillsLoaded { count }, + ) + } + }; + let _ = skills_tx.send(adapted); + }); // ── 2. Send Ready ──────────────────────────────────────────── let app_state = self.engine.app_state(); diff --git a/crates/claude-code-rs/src/ipc/subsystem_handlers.rs b/crates/claude-code-rs/src/ipc/subsystem_handlers.rs index c95e8013..2cd057f8 100644 --- a/crates/claude-code-rs/src/ipc/subsystem_handlers.rs +++ b/crates/claude-code-rs/src/ipc/subsystem_handlers.rs @@ -160,7 +160,10 @@ pub fn handle_skill_command(cmd: super::subsystem_events::SkillCommand) -> Vec { let cwd = std::env::current_dir().ok(); crate::skills::clear_skills(); - crate::skills::init_skills(cwd.as_deref()); + crate::skills::init_skills( + &crate::config::paths::skills_dir_global(), + cwd.as_deref(), + ); let count = crate::skills::get_all_skills().len(); tracing::info!(count, "Skills reloaded via IPC"); vec![BackendMessage::SkillEvent { diff --git a/crates/claude-code-rs/src/main.rs b/crates/claude-code-rs/src/main.rs index ac10b37c..7c07a095 100644 --- a/crates/claude-code-rs/src/main.rs +++ b/crates/claude-code-rs/src/main.rs @@ -15,8 +15,10 @@ // headless / TUI / print / json). // ============================================================================ -// Process-wide bootstrap singleton layer (import DAG leaf node) -mod bootstrap; +// Process-wide bootstrap singleton layer (import DAG leaf node). +// Lives in its own crate (`cc-bootstrap`). Re-alias so existing +// `crate::bootstrap::...` paths continue to resolve. +use cc_bootstrap as bootstrap; // Core modules mod cli; @@ -41,12 +43,14 @@ mod voice; // Context compaction pipeline mod compact; -// Network / API / auth +// Network / API / auth. `auth` lives in its own crate (`cc-auth`); re-alias +// so existing `crate::auth::...` paths continue to resolve. mod api; -mod auth; +use cc_auth as auth; -// Skills system -mod skills; +// Skills system lives in its own crate (`cc-skills`). Re-alias so existing +// `crate::skills::...` paths continue to resolve. +use cc_skills as skills; // Plugin system mod plugins; @@ -133,6 +137,11 @@ use crate::ui::tui; fn main() -> ExitCode { startup::load_env_files(); + // Wire `cc-auth`'s credentials path — it lives outside the root crate now + // (P2, issue #71) so it can't call `crate::config::paths::credentials_path()` + // directly. Register once, before any fast path might hit OAuth resolution. + cc_auth::set_credentials_path(crate::config::paths::credentials_path()); + // Phase A: parse args first so fast paths can exit immediately let cli = Cli::parse(); @@ -246,7 +255,10 @@ async fn run_full_init(cli: Cli) -> anyhow::Result { // B.3c: Initialize skills (bundled/user/project + plugin) skills::clear_skills(); - skills::init_skills(Some(std::path::Path::new(&cwd))); + skills::init_skills( + &crate::config::paths::skills_dir_global(), + Some(std::path::Path::new(&cwd)), + ); let plugin_skills = plugins::discover_plugin_skills(); if !plugin_skills.is_empty() { info!(