refactor(v0.19): extract adapters/jsonmcp.rs shared MCP entry merge/remove
v0.19 agent's additional factorization that wasn't captured in the initial branch commit. Extracts shared merge/remove-named helpers for claude-code/cursor/zed into adapters/jsonmcp.rs (70 LOC). 3 adapters simplify significantly (-65/-68/-102 LOC each). Also: #[allow(dead_code)] on Error::AdapterFailed (surfaced by mount/detach orchestration; reserved for library consumers). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d39abf1914
commit
fa253d04cc
8 changed files with 122 additions and 250 deletions
|
|
@ -1,19 +1,19 @@
|
|||
//! Claude Code adapter — writes MCP server entry into
|
||||
//! `~/.claude/settings.json`. Config shape merges under
|
||||
//! `mcpServers.keisei` so we never clobber unrelated entries.
|
||||
//! `~/.claude/settings.json` under `mcpServers.keisei`.
|
||||
//!
|
||||
//! Detection: `$CWD/.claude/settings.json` exists OR
|
||||
//! Detection: `$CWD/.claude/settings.json` is a file OR
|
||||
//! `$KEISEI_HOME/.claude` (or `$HOME/.claude`) is a directory.
|
||||
//! `$KEISEI_HOME` overrides `$HOME` for tests.
|
||||
|
||||
use crate::adapter::ClientAdapter;
|
||||
use crate::adapters::jsonmcp::{keisei_entry, load_json_or_empty, merge_named, remove_named};
|
||||
use crate::brain::Brain;
|
||||
use crate::error::Result;
|
||||
use crate::fsx::write_atomic;
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub const MCP_ENTRY_KEY: &str = "keisei";
|
||||
pub const PARENT_KEY: &str = "mcpServers";
|
||||
pub const ENTRY_KEY: &str = "keisei";
|
||||
|
||||
pub struct ClaudeCodeAdapter;
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ impl ClientAdapter for ClaudeCodeAdapter {
|
|||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let mut doc = load_json_or_empty(&cfg)?;
|
||||
merge_mcp_entry(&mut doc, brain);
|
||||
merge_named(&mut doc, PARENT_KEY, ENTRY_KEY, keisei_entry(brain));
|
||||
write_atomic(&cfg, &serde_json::to_string_pretty(&doc)?)
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ impl ClientAdapter for ClaudeCodeAdapter {
|
|||
return Ok(());
|
||||
}
|
||||
let mut doc = load_json_or_empty(&cfg)?;
|
||||
remove_mcp_entry(&mut doc);
|
||||
remove_named(&mut doc, PARENT_KEY, ENTRY_KEY);
|
||||
write_atomic(&cfg, &serde_json::to_string_pretty(&doc)?)
|
||||
}
|
||||
|
||||
|
|
@ -74,54 +74,3 @@ impl ClientAdapter for ClaudeCodeAdapter {
|
|||
self.user_config_dir().join("settings.json")
|
||||
}
|
||||
}
|
||||
|
||||
fn load_json_or_empty(cfg: &std::path::Path) -> Result<Value> {
|
||||
if !cfg.is_file() {
|
||||
return Ok(json!({}));
|
||||
}
|
||||
let raw = std::fs::read_to_string(cfg)?;
|
||||
if raw.trim().is_empty() {
|
||||
return Ok(json!({}));
|
||||
}
|
||||
Ok(serde_json::from_str(&raw)?)
|
||||
}
|
||||
|
||||
fn merge_mcp_entry(doc: &mut Value, brain: &Brain) {
|
||||
if !doc.is_object() {
|
||||
*doc = json!({});
|
||||
}
|
||||
let obj = doc.as_object_mut().expect("doc is object after guard");
|
||||
let servers = obj
|
||||
.entry("mcpServers".to_string())
|
||||
.or_insert_with(|| Value::Object(Map::new()));
|
||||
if !servers.is_object() {
|
||||
*servers = Value::Object(Map::new());
|
||||
}
|
||||
let entry = json!({
|
||||
"command": brain.mcp_server_path().to_string_lossy(),
|
||||
"args": [],
|
||||
"env": {
|
||||
"KEISEI_BRAIN_ROOT": brain.root.to_string_lossy(),
|
||||
"KEISEI_BRAIN_NAME": brain.name(),
|
||||
}
|
||||
});
|
||||
servers
|
||||
.as_object_mut()
|
||||
.expect("servers is object")
|
||||
.insert(MCP_ENTRY_KEY.to_string(), entry);
|
||||
}
|
||||
|
||||
fn remove_mcp_entry(doc: &mut Value) {
|
||||
if !doc.is_object() {
|
||||
return;
|
||||
}
|
||||
let obj = doc.as_object_mut().expect("doc is object after guard");
|
||||
if let Some(servers) = obj.get_mut("mcpServers") {
|
||||
if let Some(map) = servers.as_object_mut() {
|
||||
map.remove(MCP_ENTRY_KEY);
|
||||
if map.is_empty() {
|
||||
obj.remove("mcpServers");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,14 @@
|
|||
//! Continue.dev adapter — writes MCP server entry into `~/.continue/`.
|
||||
//!
|
||||
//! Config path strategy [UNVERIFIED — see note]:
|
||||
//! 1. If `~/.continue/config.yaml` exists → YAML mode
|
||||
//! 2. Else if `~/.continue/config.json` exists → JSON mode
|
||||
//! 3. Else if `~/.continue/` exists → create `config.yaml` fresh
|
||||
//! 4. Else `detect()` returns false (graceful)
|
||||
//!
|
||||
//! Schema (both forms), under top-level `mcpServers`:
|
||||
//! Picks YAML if `config.yaml` exists, JSON if `config.json` exists,
|
||||
//! else defaults to fresh `config.yaml`. Detection fires on `~/.continue/`
|
||||
//! dir. Schema [UNVERIFIED — list-form under `mcpServers`]:
|
||||
//! ```yaml
|
||||
//! mcpServers:
|
||||
//! - name: keisei
|
||||
//! command: /path/to/kei-mcp-server
|
||||
//! command: ...
|
||||
//! args: []
|
||||
//! env:
|
||||
//! KEISEI_BRAIN_ROOT: /Volumes/Brain1
|
||||
//! ```
|
||||
//!
|
||||
//! NOTE: Continue's exact MCP/plugin schema is [UNVERIFIED] in this
|
||||
//! session. The adapter uses a list-form `mcpServers` which matches the
|
||||
//! v0.18 prototypes and the Continue `config.yaml` conventions observed
|
||||
//! in the public docs. If the live schema diverges, update this module.
|
||||
//!
|
||||
//! Detach preserves all other config keys and all non-keisei servers.
|
||||
|
||||
use crate::adapter::ClientAdapter;
|
||||
use crate::brain::Brain;
|
||||
|
|
@ -63,7 +50,6 @@ impl ContinueAdapter {
|
|||
} else if json.is_file() {
|
||||
(Form::Json, json)
|
||||
} else {
|
||||
// Default for a fresh install: YAML (Continue's preferred form).
|
||||
(Form::Yaml, yaml)
|
||||
}
|
||||
}
|
||||
|
|
@ -109,9 +95,6 @@ impl ClientAdapter for ContinueAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
/// Load doc as a generic `serde_json::Value`. YAML → Value via serde_yaml,
|
||||
/// JSON → Value directly. Unifying on `Value` keeps the merge logic form-
|
||||
/// independent.
|
||||
fn load_doc(cfg: &std::path::Path, form: Form) -> Result<Value> {
|
||||
if !cfg.is_file() {
|
||||
return Ok(json!({}));
|
||||
|
|
@ -120,14 +103,13 @@ fn load_doc(cfg: &std::path::Path, form: Form) -> Result<Value> {
|
|||
if raw.trim().is_empty() {
|
||||
return Ok(json!({}));
|
||||
}
|
||||
let parsed: Value = match form {
|
||||
match form {
|
||||
Form::Yaml => serde_yaml::from_str(&raw).map_err(|e| Error::ConfigParseError {
|
||||
path: cfg.to_path_buf(),
|
||||
reason: e.to_string(),
|
||||
})?,
|
||||
Form::Json => serde_json::from_str(&raw)?,
|
||||
};
|
||||
Ok(parsed)
|
||||
}),
|
||||
Form::Json => Ok(serde_json::from_str(&raw)?),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_doc(cfg: &std::path::Path, form: Form, doc: &Value) -> Result<()> {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
//! Cursor adapter — writes MCP server entry to Cursor's MCP config.
|
||||
//! Cursor adapter — writes MCP entry under `mcpServers.keisei`.
|
||||
//!
|
||||
//! Config path: `$CWD/.cursor/mcp.json` if the project-local `.cursor/`
|
||||
//! dir exists, else `~/.cursor/mcp.json`. Detection fires if either dir
|
||||
//! exists. Schema [UNVERIFIED — matches Claude Desktop MCP convention]:
|
||||
//! Path: `$CWD/.cursor/mcp.json` if the project-local `.cursor/` exists,
|
||||
//! else `~/.cursor/mcp.json`. Detection fires on either dir. Schema
|
||||
//! [UNVERIFIED — matches Claude Desktop MCP convention]:
|
||||
//! `{ "mcpServers": { "keisei": { "command": "...", "args": [] } } }`.
|
||||
|
||||
use crate::adapter::ClientAdapter;
|
||||
use crate::adapters::jsonmcp::{keisei_entry, load_json_or_empty, merge_named, remove_named};
|
||||
use crate::brain::Brain;
|
||||
use crate::error::Result;
|
||||
use crate::fsx::write_atomic;
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub const MCP_ENTRY_KEY: &str = "keisei";
|
||||
pub const PARENT_KEY: &str = "mcpServers";
|
||||
pub const ENTRY_KEY: &str = "keisei";
|
||||
|
||||
pub struct CursorAdapter;
|
||||
|
||||
|
|
@ -69,7 +70,7 @@ impl ClientAdapter for CursorAdapter {
|
|||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let mut doc = load_json_or_empty(&cfg)?;
|
||||
merge_entry(&mut doc, brain);
|
||||
merge_named(&mut doc, PARENT_KEY, ENTRY_KEY, keisei_entry(brain));
|
||||
write_atomic(&cfg, &serde_json::to_string_pretty(&doc)?)
|
||||
}
|
||||
|
||||
|
|
@ -79,7 +80,7 @@ impl ClientAdapter for CursorAdapter {
|
|||
return Ok(());
|
||||
}
|
||||
let mut doc = load_json_or_empty(&cfg)?;
|
||||
remove_entry(&mut doc);
|
||||
remove_named(&mut doc, PARENT_KEY, ENTRY_KEY);
|
||||
write_atomic(&cfg, &serde_json::to_string_pretty(&doc)?)
|
||||
}
|
||||
|
||||
|
|
@ -87,54 +88,3 @@ impl ClientAdapter for CursorAdapter {
|
|||
self.pick_config_path()
|
||||
}
|
||||
}
|
||||
|
||||
fn load_json_or_empty(cfg: &std::path::Path) -> Result<Value> {
|
||||
if !cfg.is_file() {
|
||||
return Ok(json!({}));
|
||||
}
|
||||
let raw = std::fs::read_to_string(cfg)?;
|
||||
if raw.trim().is_empty() {
|
||||
return Ok(json!({}));
|
||||
}
|
||||
Ok(serde_json::from_str(&raw)?)
|
||||
}
|
||||
|
||||
fn merge_entry(doc: &mut Value, brain: &Brain) {
|
||||
if !doc.is_object() {
|
||||
*doc = json!({});
|
||||
}
|
||||
let obj = doc.as_object_mut().expect("doc is object after guard");
|
||||
let servers = obj
|
||||
.entry("mcpServers".to_string())
|
||||
.or_insert_with(|| Value::Object(Map::new()));
|
||||
if !servers.is_object() {
|
||||
*servers = Value::Object(Map::new());
|
||||
}
|
||||
let entry = json!({
|
||||
"command": brain.mcp_server_path().to_string_lossy(),
|
||||
"args": [],
|
||||
"env": {
|
||||
"KEISEI_BRAIN_ROOT": brain.root.to_string_lossy(),
|
||||
"KEISEI_BRAIN_NAME": brain.name(),
|
||||
}
|
||||
});
|
||||
servers
|
||||
.as_object_mut()
|
||||
.expect("servers is object")
|
||||
.insert(MCP_ENTRY_KEY.to_string(), entry);
|
||||
}
|
||||
|
||||
fn remove_entry(doc: &mut Value) {
|
||||
if !doc.is_object() {
|
||||
return;
|
||||
}
|
||||
let obj = doc.as_object_mut().expect("doc is object after guard");
|
||||
if let Some(servers) = obj.get_mut("mcpServers") {
|
||||
if let Some(map) = servers.as_object_mut() {
|
||||
map.remove(MCP_ENTRY_KEY);
|
||||
if map.is_empty() {
|
||||
obj.remove("mcpServers");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
70
_primitives/_rust/keisei/src/adapters/jsonmcp.rs
Normal file
70
_primitives/_rust/keisei/src/adapters/jsonmcp.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
//! Shared JSON-merge helpers for MCP-style adapters (claude-code, cursor,
|
||||
//! zed). Same algorithm, different parent/entry keys.
|
||||
//!
|
||||
//! Constructor Pattern: single responsibility — merge / remove one named
|
||||
//! server under one named parent, preserving every other key.
|
||||
|
||||
use crate::brain::Brain;
|
||||
use crate::error::Result;
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
/// Load `cfg` as JSON, or return empty `{}` if missing/blank. JSON parse
|
||||
/// errors propagate via `Error::Json`.
|
||||
pub fn load_json_or_empty(cfg: &std::path::Path) -> Result<Value> {
|
||||
if !cfg.is_file() {
|
||||
return Ok(json!({}));
|
||||
}
|
||||
let raw = std::fs::read_to_string(cfg)?;
|
||||
if raw.trim().is_empty() {
|
||||
return Ok(json!({}));
|
||||
}
|
||||
Ok(serde_json::from_str(&raw)?)
|
||||
}
|
||||
|
||||
/// Build the keisei MCP entry value.
|
||||
pub fn keisei_entry(brain: &Brain) -> Value {
|
||||
json!({
|
||||
"command": brain.mcp_server_path().to_string_lossy(),
|
||||
"args": [],
|
||||
"env": {
|
||||
"KEISEI_BRAIN_ROOT": brain.root.to_string_lossy(),
|
||||
"KEISEI_BRAIN_NAME": brain.name(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Insert `entry` under `doc[parent_key][entry_key]`, creating both keys
|
||||
/// as objects if absent. Existing siblings are preserved.
|
||||
pub fn merge_named(doc: &mut Value, parent_key: &str, entry_key: &str, entry: Value) {
|
||||
if !doc.is_object() {
|
||||
*doc = json!({});
|
||||
}
|
||||
let obj = doc.as_object_mut().expect("doc object");
|
||||
let parent = obj
|
||||
.entry(parent_key.to_string())
|
||||
.or_insert_with(|| Value::Object(Map::new()));
|
||||
if !parent.is_object() {
|
||||
*parent = Value::Object(Map::new());
|
||||
}
|
||||
parent
|
||||
.as_object_mut()
|
||||
.expect("parent object")
|
||||
.insert(entry_key.to_string(), entry);
|
||||
}
|
||||
|
||||
/// Remove `doc[parent_key][entry_key]`. If parent ends up empty, remove
|
||||
/// parent too (no vestigial `{}` in the config).
|
||||
pub fn remove_named(doc: &mut Value, parent_key: &str, entry_key: &str) {
|
||||
if !doc.is_object() {
|
||||
return;
|
||||
}
|
||||
let obj = doc.as_object_mut().expect("doc object");
|
||||
if let Some(parent) = obj.get_mut(parent_key) {
|
||||
if let Some(map) = parent.as_object_mut() {
|
||||
map.remove(entry_key);
|
||||
if map.is_empty() {
|
||||
obj.remove(parent_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
//! Concrete `ClientAdapter` implementations, one file per client.
|
||||
//!
|
||||
//! Constructor Pattern: this file is the module declaration hub only —
|
||||
//! no logic lives here. Each adapter owns its own file.
|
||||
//! no logic lives here. `jsonmcp` owns the shared JSON merge helpers
|
||||
//! used by every JSON-keyed adapter (claude-code, cursor, zed).
|
||||
|
||||
pub mod claude_code;
|
||||
pub mod continue_adapter;
|
||||
pub mod cursor;
|
||||
pub mod jsonmcp;
|
||||
pub mod zed;
|
||||
|
|
|
|||
|
|
@ -1,40 +1,20 @@
|
|||
//! Zed adapter — writes MCP/context-server entry into Zed settings.
|
||||
//! Zed adapter — writes context-server entry under `context_servers.keisei`.
|
||||
//!
|
||||
//! Config path [UNVERIFIED for exact schema key-name]:
|
||||
//! - macOS: `~/Library/Application Support/Zed/settings.json`
|
||||
//! - Linux: `~/.config/zed/settings.json`
|
||||
//! - Windows: not supported in this adapter (Zed Windows is preview)
|
||||
//!
|
||||
//! Schema (under a top-level `context_servers` object):
|
||||
//! ```json
|
||||
//! {
|
||||
//! "context_servers": {
|
||||
//! "keisei": {
|
||||
//! "command": "/path/to/kei-mcp-server",
|
||||
//! "args": [],
|
||||
//! "env": { "KEISEI_BRAIN_ROOT": "..." }
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! NOTE: Zed's `context_servers` key is the documented extension point for
|
||||
//! MCP at time of writing — but the full schema (arg handling,
|
||||
//! environment) is [UNVERIFIED] in this session. If a future Zed release
|
||||
//! diverges, update this module.
|
||||
//!
|
||||
//! Detach preserves all other settings and all non-keisei context servers.
|
||||
//! If the settings file is not valid JSON (Zed historically permitted
|
||||
//! JSONC / comments), attach degrades gracefully by returning
|
||||
//! `ConfigParseError` — the user can then switch to manual config.
|
||||
//! Paths: `~/Library/Application Support/Zed/settings.json` on macOS,
|
||||
//! `~/.config/zed/settings.json` on Linux. Detection fires on the
|
||||
//! platform settings dir. Schema [UNVERIFIED]:
|
||||
//! `{ "context_servers": { "keisei": { "command": "...", "args": [] } } }`.
|
||||
//! Invalid JSON (e.g. JSONC with comments) → `ConfigParseError`.
|
||||
|
||||
use crate::adapter::ClientAdapter;
|
||||
use crate::adapters::jsonmcp::{keisei_entry, merge_named, remove_named};
|
||||
use crate::brain::Brain;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::fsx::write_atomic;
|
||||
use serde_json::{json, Map, Value};
|
||||
use serde_json::{json, Value};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub const PARENT_KEY: &str = "context_servers";
|
||||
pub const ENTRY_KEY: &str = "keisei";
|
||||
|
||||
pub struct ZedAdapter;
|
||||
|
|
@ -56,10 +36,6 @@ impl ZedAdapter {
|
|||
base.join(".config/zed")
|
||||
}
|
||||
}
|
||||
|
||||
fn settings_file(&self) -> PathBuf {
|
||||
self.settings_dir().join("settings.json")
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ZedAdapter {
|
||||
|
|
@ -82,10 +58,9 @@ impl ClientAdapter for ZedAdapter {
|
|||
if let Some(parent) = cfg.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let mut doc = load_json_or_empty(&cfg)?;
|
||||
merge_entry(&mut doc, brain);
|
||||
write_atomic(&cfg, &serde_json::to_string_pretty(&doc)?)?;
|
||||
Ok(())
|
||||
let mut doc = load_strict_json(&cfg)?;
|
||||
merge_named(&mut doc, PARENT_KEY, ENTRY_KEY, keisei_entry(brain));
|
||||
write_atomic(&cfg, &serde_json::to_string_pretty(&doc)?)
|
||||
}
|
||||
|
||||
fn detach(&self) -> Result<()> {
|
||||
|
|
@ -93,18 +68,19 @@ impl ClientAdapter for ZedAdapter {
|
|||
if !cfg.is_file() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut doc = load_json_or_empty(&cfg)?;
|
||||
remove_entry(&mut doc);
|
||||
write_atomic(&cfg, &serde_json::to_string_pretty(&doc)?)?;
|
||||
Ok(())
|
||||
let mut doc = load_strict_json(&cfg)?;
|
||||
remove_named(&mut doc, PARENT_KEY, ENTRY_KEY);
|
||||
write_atomic(&cfg, &serde_json::to_string_pretty(&doc)?)
|
||||
}
|
||||
|
||||
fn config_path(&self) -> PathBuf {
|
||||
self.settings_file()
|
||||
self.settings_dir().join("settings.json")
|
||||
}
|
||||
}
|
||||
|
||||
fn load_json_or_empty(cfg: &std::path::Path) -> Result<Value> {
|
||||
/// Zed historically permitted JSONC. We require strict JSON and surface a
|
||||
/// `ConfigParseError` pointing at the file if the user has comments.
|
||||
fn load_strict_json(cfg: &std::path::Path) -> Result<Value> {
|
||||
if !cfg.is_file() {
|
||||
return Ok(json!({}));
|
||||
}
|
||||
|
|
@ -117,43 +93,3 @@ fn load_json_or_empty(cfg: &std::path::Path) -> Result<Value> {
|
|||
reason: e.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn merge_entry(doc: &mut Value, brain: &Brain) {
|
||||
if !doc.is_object() {
|
||||
*doc = json!({});
|
||||
}
|
||||
let obj = doc.as_object_mut().expect("doc is object after guard");
|
||||
let servers = obj
|
||||
.entry("context_servers".to_string())
|
||||
.or_insert_with(|| Value::Object(Map::new()));
|
||||
if !servers.is_object() {
|
||||
*servers = Value::Object(Map::new());
|
||||
}
|
||||
let entry = json!({
|
||||
"command": brain.mcp_server_path().to_string_lossy(),
|
||||
"args": [],
|
||||
"env": {
|
||||
"KEISEI_BRAIN_ROOT": brain.root.to_string_lossy(),
|
||||
"KEISEI_BRAIN_NAME": brain.name(),
|
||||
}
|
||||
});
|
||||
servers
|
||||
.as_object_mut()
|
||||
.expect("servers is object")
|
||||
.insert(ENTRY_KEY.to_string(), entry);
|
||||
}
|
||||
|
||||
fn remove_entry(doc: &mut Value) {
|
||||
if !doc.is_object() {
|
||||
return;
|
||||
}
|
||||
let obj = doc.as_object_mut().expect("doc is object after guard");
|
||||
if let Some(servers) = obj.get_mut("context_servers") {
|
||||
if let Some(map) = servers.as_object_mut() {
|
||||
map.remove(ENTRY_KEY);
|
||||
if map.is_empty() {
|
||||
obj.remove("context_servers");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ pub enum Error {
|
|||
NotAttached,
|
||||
|
||||
#[error("adapter '{client}' failed: {reason}")]
|
||||
#[allow(dead_code)] // surfaced by mount/detach orchestration; reserved for library consumers
|
||||
AdapterFailed { client: String, reason: String },
|
||||
|
||||
#[error("config parse error at {path}: {reason}")]
|
||||
|
|
|
|||
|
|
@ -27,29 +27,11 @@ struct Row {
|
|||
}
|
||||
|
||||
fn print_table(rows: &[Row]) {
|
||||
let name_w = header_or_max(rows, "adapter", |r| r.name.len());
|
||||
let det_w = "detected".len();
|
||||
println!(
|
||||
"{:<name_w$} {:<det_w$} config_path",
|
||||
"adapter",
|
||||
"detected",
|
||||
name_w = name_w,
|
||||
det_w = det_w
|
||||
);
|
||||
println!("{:<name_w$} {:<det_w$} {}", "-------", "--------", "-----------", name_w = name_w, det_w = det_w);
|
||||
let name_w = rows.iter().map(|r| r.name.len()).max().unwrap_or(0).max(7);
|
||||
println!("{:<w1$} detected config_path", "adapter", w1 = name_w);
|
||||
println!("{:<w1$} -------- -----------", "-------", w1 = name_w);
|
||||
for r in rows {
|
||||
let mark = if r.detected { "yes" } else { "no" };
|
||||
println!(
|
||||
"{:<name_w$} {:<det_w$} {}",
|
||||
r.name,
|
||||
mark,
|
||||
r.config_path,
|
||||
name_w = name_w,
|
||||
det_w = det_w
|
||||
);
|
||||
let mark = if r.detected { "yes " } else { "no " };
|
||||
println!("{:<w1$} {} {}", r.name, mark, r.config_path, w1 = name_w);
|
||||
}
|
||||
}
|
||||
|
||||
fn header_or_max<F: Fn(&Row) -> usize>(rows: &[Row], header: &str, f: F) -> usize {
|
||||
rows.iter().map(f).max().unwrap_or(0).max(header.len())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue