feat(error): add Rust AppError enum and migrate command modules
- AppError enum with 10 variants (Database, Auth, Filesystem, Ipc, NotFound,
Validation, Sidecar, Config, Network, Internal) + serde tag serialization
- From impls for rusqlite::Error, std::io::Error, serde_json::Error
- Migrated 9 command modules from Result<T, String> to Result<T, AppError>
- Frontend receives structured {kind, detail} objects via IPC
This commit is contained in:
parent
365c420901
commit
8b3b0ab720
11 changed files with 319 additions and 81 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
use crate::error::AppError;
|
||||||
use crate::sidecar::AgentQueryOptions;
|
use crate::sidecar::AgentQueryOptions;
|
||||||
use agor_core::sandbox::SandboxConfig;
|
use agor_core::sandbox::SandboxConfig;
|
||||||
|
|
||||||
|
|
@ -8,18 +9,18 @@ use agor_core::sandbox::SandboxConfig;
|
||||||
pub fn agent_query(
|
pub fn agent_query(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
options: AgentQueryOptions,
|
options: AgentQueryOptions,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), AppError> {
|
||||||
// Run pre-dispatch hooks (budget enforcement, branch policy, etc.)
|
// Run pre-dispatch hooks (budget enforcement, branch policy, etc.)
|
||||||
for hook in &state.pre_dispatch_hooks {
|
for hook in &state.pre_dispatch_hooks {
|
||||||
hook(&options)?;
|
hook(&options).map_err(AppError::sidecar)?;
|
||||||
}
|
}
|
||||||
state.sidecar_manager.query(&options)
|
state.sidecar_manager.query(&options).map_err(AppError::sidecar)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[tracing::instrument(skip(state))]
|
#[tracing::instrument(skip(state))]
|
||||||
pub fn agent_stop(state: State<'_, AppState>, session_id: String) -> Result<(), String> {
|
pub fn agent_stop(state: State<'_, AppState>, session_id: String) -> Result<(), AppError> {
|
||||||
state.sidecar_manager.stop_session(&session_id)
|
state.sidecar_manager.stop_session(&session_id).map_err(AppError::sidecar)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|
@ -29,8 +30,8 @@ pub fn agent_ready(state: State<'_, AppState>) -> bool {
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[tracing::instrument(skip(state))]
|
#[tracing::instrument(skip(state))]
|
||||||
pub fn agent_restart(state: State<'_, AppState>) -> Result<(), String> {
|
pub fn agent_restart(state: State<'_, AppState>) -> Result<(), AppError> {
|
||||||
state.sidecar_manager.restart()
|
state.sidecar_manager.restart().map_err(AppError::sidecar)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update sidecar sandbox configuration and restart to apply.
|
/// Update sidecar sandbox configuration and restart to apply.
|
||||||
|
|
@ -44,7 +45,7 @@ pub fn agent_set_sandbox(
|
||||||
project_cwds: Vec<String>,
|
project_cwds: Vec<String>,
|
||||||
worktree_roots: Vec<String>,
|
worktree_roots: Vec<String>,
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), AppError> {
|
||||||
let cwd_refs: Vec<&str> = project_cwds.iter().map(|s| s.as_str()).collect();
|
let cwd_refs: Vec<&str> = project_cwds.iter().map(|s| s.as_str()).collect();
|
||||||
let wt_refs: Vec<&str> = worktree_roots.iter().map(|s| s.as_str()).collect();
|
let wt_refs: Vec<&str> = worktree_roots.iter().map(|s| s.as_str()).collect();
|
||||||
|
|
||||||
|
|
@ -55,7 +56,7 @@ pub fn agent_set_sandbox(
|
||||||
|
|
||||||
// Restart sidecar so Landlock restrictions take effect on the new process
|
// Restart sidecar so Landlock restrictions take effect on the new process
|
||||||
if state.sidecar_manager.is_ready() {
|
if state.sidecar_manager.is_ready() {
|
||||||
state.sidecar_manager.restart()?;
|
state.sidecar_manager.restart().map_err(AppError::sidecar)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
// Claude profile and skill discovery commands
|
// Claude profile and skill discovery commands
|
||||||
|
|
||||||
|
use crate::error::AppError;
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
pub struct ClaudeProfile {
|
pub struct ClaudeProfile {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
@ -142,17 +144,18 @@ pub fn claude_list_skills() -> Vec<ClaudeSkill> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn claude_read_skill(path: String) -> Result<String, String> {
|
pub fn claude_read_skill(path: String) -> Result<String, AppError> {
|
||||||
let skills_dir = dirs::home_dir()
|
let skills_dir = dirs::home_dir()
|
||||||
.ok_or("Cannot determine home directory")?
|
.ok_or_else(|| AppError::config("Cannot determine home directory"))?
|
||||||
.join(".claude")
|
.join(".claude")
|
||||||
.join("skills");
|
.join("skills");
|
||||||
let canonical_skills = skills_dir.canonicalize()
|
let canonical_skills = skills_dir.canonicalize()
|
||||||
.map_err(|_| "Skills directory does not exist".to_string())?;
|
.map_err(|_| AppError::not_found("Skills directory does not exist"))?;
|
||||||
let canonical_path = std::path::Path::new(&path).canonicalize()
|
let canonical_path = std::path::Path::new(&path).canonicalize()
|
||||||
.map_err(|e| format!("Invalid skill path: {e}"))?;
|
.map_err(|e| AppError::filesystem(format!("Invalid skill path: {e}")))?;
|
||||||
if !canonical_path.starts_with(&canonical_skills) {
|
if !canonical_path.starts_with(&canonical_skills) {
|
||||||
return Err("Access denied: path is outside skills directory".to_string());
|
return Err(AppError::auth("Access denied: path is outside skills directory"));
|
||||||
}
|
}
|
||||||
std::fs::read_to_string(&canonical_path).map_err(|e| format!("Failed to read skill: {e}"))
|
std::fs::read_to_string(&canonical_path)
|
||||||
|
.map_err(|e| AppError::filesystem(format!("Failed to read skill: {e}")))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
// File browser commands (Files tab)
|
// File browser commands (Files tab)
|
||||||
|
|
||||||
|
use crate::error::AppError;
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
pub struct DirEntry {
|
pub struct DirEntry {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
@ -19,16 +21,18 @@ pub enum FileContent {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn list_directory_children(path: String) -> Result<Vec<DirEntry>, String> {
|
pub fn list_directory_children(path: String) -> Result<Vec<DirEntry>, AppError> {
|
||||||
let dir = std::path::Path::new(&path);
|
let dir = std::path::Path::new(&path);
|
||||||
if !dir.is_dir() {
|
if !dir.is_dir() {
|
||||||
return Err(format!("Not a directory: {path}"));
|
return Err(AppError::not_found(format!("Not a directory: {path}")));
|
||||||
}
|
}
|
||||||
let mut entries = Vec::new();
|
let mut entries = Vec::new();
|
||||||
let read_dir = std::fs::read_dir(dir).map_err(|e| format!("Failed to read directory: {e}"))?;
|
let read_dir = std::fs::read_dir(dir)
|
||||||
|
.map_err(|e| AppError::filesystem(format!("Failed to read directory: {e}")))?;
|
||||||
for entry in read_dir {
|
for entry in read_dir {
|
||||||
let entry = entry.map_err(|e| format!("Failed to read entry: {e}"))?;
|
let entry = entry.map_err(|e| AppError::filesystem(format!("Failed to read entry: {e}")))?;
|
||||||
let metadata = entry.metadata().map_err(|e| format!("Failed to read metadata: {e}"))?;
|
let metadata = entry.metadata()
|
||||||
|
.map_err(|e| AppError::filesystem(format!("Failed to read metadata: {e}")))?;
|
||||||
let name = entry.file_name().to_string_lossy().into_owned();
|
let name = entry.file_name().to_string_lossy().into_owned();
|
||||||
if name.starts_with('.') {
|
if name.starts_with('.') {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -57,12 +61,13 @@ pub fn list_directory_children(path: String) -> Result<Vec<DirEntry>, String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn read_file_content(path: String) -> Result<FileContent, String> {
|
pub fn read_file_content(path: String) -> Result<FileContent, AppError> {
|
||||||
let file_path = std::path::Path::new(&path);
|
let file_path = std::path::Path::new(&path);
|
||||||
if !file_path.is_file() {
|
if !file_path.is_file() {
|
||||||
return Err(format!("Not a file: {path}"));
|
return Err(AppError::not_found(format!("Not a file: {path}")));
|
||||||
}
|
}
|
||||||
let metadata = std::fs::metadata(&path).map_err(|e| format!("Failed to read metadata: {e}"))?;
|
let metadata = std::fs::metadata(&path)
|
||||||
|
.map_err(|e| AppError::filesystem(format!("Failed to read metadata: {e}")))?;
|
||||||
let size = metadata.len();
|
let size = metadata.len();
|
||||||
|
|
||||||
if size > 10 * 1024 * 1024 {
|
if size > 10 * 1024 * 1024 {
|
||||||
|
|
@ -84,7 +89,7 @@ pub fn read_file_content(path: String) -> Result<FileContent, String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = std::fs::read_to_string(&path)
|
let content = std::fs::read_to_string(&path)
|
||||||
.map_err(|_| format!("Binary or non-UTF-8 file"))?;
|
.map_err(|_| AppError::validation("Binary or non-UTF-8 file"))?;
|
||||||
|
|
||||||
let lang = match ext.as_str() {
|
let lang = match ext.as_str() {
|
||||||
"rs" => "rust",
|
"rs" => "rust",
|
||||||
|
|
@ -111,17 +116,17 @@ pub fn read_file_content(path: String) -> Result<FileContent, String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn write_file_content(path: String, content: String) -> Result<(), String> {
|
pub fn write_file_content(path: String, content: String) -> Result<(), AppError> {
|
||||||
let file_path = std::path::Path::new(&path);
|
let file_path = std::path::Path::new(&path);
|
||||||
if !file_path.is_file() {
|
if !file_path.is_file() {
|
||||||
return Err(format!("Not an existing file: {path}"));
|
return Err(AppError::not_found(format!("Not an existing file: {path}")));
|
||||||
}
|
}
|
||||||
std::fs::write(&path, content.as_bytes())
|
std::fs::write(&path, content.as_bytes())
|
||||||
.map_err(|e| format!("Failed to write file: {e}"))
|
.map_err(|e| AppError::filesystem(format!("Failed to write file: {e}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn pick_directory(window: tauri::Window) -> Result<Option<String>, String> {
|
pub async fn pick_directory(window: tauri::Window) -> Result<Option<String>, AppError> {
|
||||||
let dialog = rfd::AsyncFileDialog::new()
|
let dialog = rfd::AsyncFileDialog::new()
|
||||||
.set_title("Select Directory")
|
.set_title("Select Directory")
|
||||||
.set_parent(&window);
|
.set_parent(&window);
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
|
use crate::error::AppError;
|
||||||
use crate::groups::{GroupsFile, MdFileEntry};
|
use crate::groups::{GroupsFile, MdFileEntry};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn groups_load() -> Result<GroupsFile, String> {
|
pub fn groups_load() -> Result<GroupsFile, AppError> {
|
||||||
crate::groups::load_groups()
|
crate::groups::load_groups().map_err(AppError::config)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn groups_save(config: GroupsFile) -> Result<(), String> {
|
pub fn groups_save(config: GroupsFile) -> Result<(), AppError> {
|
||||||
crate::groups::save_groups(&config)
|
crate::groups::save_groups(&config).map_err(AppError::config)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn discover_markdown_files(cwd: String) -> Result<Vec<MdFileEntry>, String> {
|
pub fn discover_markdown_files(cwd: String) -> Result<Vec<MdFileEntry>, AppError> {
|
||||||
crate::groups::discover_markdown_files(&cwd)
|
crate::groups::discover_markdown_files(&cwd).map_err(AppError::filesystem)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
use crate::error::AppError;
|
||||||
use crate::session::{AgentMessageRecord, ProjectAgentState, SessionMetric, SessionAnchorRecord};
|
use crate::session::{AgentMessageRecord, ProjectAgentState, SessionMetric, SessionAnchorRecord};
|
||||||
|
|
||||||
// --- Agent message persistence ---
|
// --- Agent message persistence ---
|
||||||
|
|
@ -11,7 +12,7 @@ pub fn agent_messages_save(
|
||||||
project_id: String,
|
project_id: String,
|
||||||
sdk_session_id: Option<String>,
|
sdk_session_id: Option<String>,
|
||||||
messages: Vec<AgentMessageRecord>,
|
messages: Vec<AgentMessageRecord>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), AppError> {
|
||||||
state.session_db.save_agent_messages(
|
state.session_db.save_agent_messages(
|
||||||
&session_id,
|
&session_id,
|
||||||
&project_id,
|
&project_id,
|
||||||
|
|
@ -24,7 +25,7 @@ pub fn agent_messages_save(
|
||||||
pub fn agent_messages_load(
|
pub fn agent_messages_load(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
project_id: String,
|
project_id: String,
|
||||||
) -> Result<Vec<AgentMessageRecord>, String> {
|
) -> Result<Vec<AgentMessageRecord>, AppError> {
|
||||||
state.session_db.load_agent_messages(&project_id)
|
state.session_db.load_agent_messages(&project_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,7 +35,7 @@ pub fn agent_messages_load(
|
||||||
pub fn project_agent_state_save(
|
pub fn project_agent_state_save(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
agent_state: ProjectAgentState,
|
agent_state: ProjectAgentState,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), AppError> {
|
||||||
state.session_db.save_project_agent_state(&agent_state)
|
state.session_db.save_project_agent_state(&agent_state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,7 +43,7 @@ pub fn project_agent_state_save(
|
||||||
pub fn project_agent_state_load(
|
pub fn project_agent_state_load(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
project_id: String,
|
project_id: String,
|
||||||
) -> Result<Option<ProjectAgentState>, String> {
|
) -> Result<Option<ProjectAgentState>, AppError> {
|
||||||
state.session_db.load_project_agent_state(&project_id)
|
state.session_db.load_project_agent_state(&project_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,7 +53,7 @@ pub fn project_agent_state_load(
|
||||||
pub fn session_metric_save(
|
pub fn session_metric_save(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
metric: SessionMetric,
|
metric: SessionMetric,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), AppError> {
|
||||||
state.session_db.save_session_metric(&metric)
|
state.session_db.save_session_metric(&metric)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,7 +62,7 @@ pub fn session_metrics_load(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
project_id: String,
|
project_id: String,
|
||||||
limit: i64,
|
limit: i64,
|
||||||
) -> Result<Vec<SessionMetric>, String> {
|
) -> Result<Vec<SessionMetric>, AppError> {
|
||||||
state.session_db.load_session_metrics(&project_id, limit)
|
state.session_db.load_session_metrics(&project_id, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,7 +72,7 @@ pub fn session_metrics_load(
|
||||||
pub fn session_anchors_save(
|
pub fn session_anchors_save(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
anchors: Vec<SessionAnchorRecord>,
|
anchors: Vec<SessionAnchorRecord>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), AppError> {
|
||||||
state.session_db.save_session_anchors(&anchors)
|
state.session_db.save_session_anchors(&anchors)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,7 +80,7 @@ pub fn session_anchors_save(
|
||||||
pub fn session_anchors_load(
|
pub fn session_anchors_load(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
project_id: String,
|
project_id: String,
|
||||||
) -> Result<Vec<SessionAnchorRecord>, String> {
|
) -> Result<Vec<SessionAnchorRecord>, AppError> {
|
||||||
state.session_db.load_session_anchors(&project_id)
|
state.session_db.load_session_anchors(&project_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,7 +88,7 @@ pub fn session_anchors_load(
|
||||||
pub fn session_anchor_delete(
|
pub fn session_anchor_delete(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
id: String,
|
id: String,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), AppError> {
|
||||||
state.session_db.delete_session_anchor(&id)
|
state.session_db.delete_session_anchor(&id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,7 +96,7 @@ pub fn session_anchor_delete(
|
||||||
pub fn session_anchors_clear(
|
pub fn session_anchors_clear(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
project_id: String,
|
project_id: String,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), AppError> {
|
||||||
state.session_db.delete_project_anchors(&project_id)
|
state.session_db.delete_project_anchors(&project_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,6 +105,6 @@ pub fn session_anchor_update_type(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
id: String,
|
id: String,
|
||||||
anchor_type: String,
|
anchor_type: String,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), AppError> {
|
||||||
state.session_db.update_anchor_type(&id, &anchor_type)
|
state.session_db.update_anchor_type(&id, &anchor_type)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
use crate::error::AppError;
|
||||||
use crate::pty::PtyOptions;
|
use crate::pty::PtyOptions;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|
@ -7,13 +8,13 @@ use crate::pty::PtyOptions;
|
||||||
pub fn pty_spawn(
|
pub fn pty_spawn(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
options: PtyOptions,
|
options: PtyOptions,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, AppError> {
|
||||||
state.pty_manager.spawn(options)
|
state.pty_manager.spawn(options).map_err(AppError::sidecar)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn pty_write(state: State<'_, AppState>, id: String, data: String) -> Result<(), String> {
|
pub fn pty_write(state: State<'_, AppState>, id: String, data: String) -> Result<(), AppError> {
|
||||||
state.pty_manager.write(&id, &data)
|
state.pty_manager.write(&id, &data).map_err(AppError::sidecar)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|
@ -22,12 +23,12 @@ pub fn pty_resize(
|
||||||
id: String,
|
id: String,
|
||||||
cols: u16,
|
cols: u16,
|
||||||
rows: u16,
|
rows: u16,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), AppError> {
|
||||||
state.pty_manager.resize(&id, cols, rows)
|
state.pty_manager.resize(&id, cols, rows).map_err(AppError::sidecar)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[tracing::instrument(skip(state))]
|
#[tracing::instrument(skip(state))]
|
||||||
pub fn pty_kill(state: State<'_, AppState>, id: String) -> Result<(), String> {
|
pub fn pty_kill(state: State<'_, AppState>, id: String) -> Result<(), AppError> {
|
||||||
state.pty_manager.kill(&id)
|
state.pty_manager.kill(&id).map_err(AppError::sidecar)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,24 @@
|
||||||
|
use crate::error::AppError;
|
||||||
use crate::secrets::SecretsManager;
|
use crate::secrets::SecretsManager;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn secrets_store(key: String, value: String) -> Result<(), String> {
|
pub fn secrets_store(key: String, value: String) -> Result<(), AppError> {
|
||||||
SecretsManager::store_secret(&key, &value)
|
SecretsManager::store_secret(&key, &value).map_err(AppError::auth)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn secrets_get(key: String) -> Result<Option<String>, String> {
|
pub fn secrets_get(key: String) -> Result<Option<String>, AppError> {
|
||||||
SecretsManager::get_secret(&key)
|
SecretsManager::get_secret(&key).map_err(AppError::auth)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn secrets_delete(key: String) -> Result<(), String> {
|
pub fn secrets_delete(key: String) -> Result<(), AppError> {
|
||||||
SecretsManager::delete_secret(&key)
|
SecretsManager::delete_secret(&key).map_err(AppError::auth)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn secrets_list() -> Result<Vec<String>, String> {
|
pub fn secrets_list() -> Result<Vec<String>, AppError> {
|
||||||
SecretsManager::list_keys()
|
SecretsManager::list_keys().map_err(AppError::auth)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|
|
||||||
|
|
@ -1,81 +1,82 @@
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
use crate::error::AppError;
|
||||||
use crate::session::{Session, LayoutState, SshSession};
|
use crate::session::{Session, LayoutState, SshSession};
|
||||||
|
|
||||||
// --- Session persistence ---
|
// --- Session persistence ---
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn session_list(state: State<'_, AppState>) -> Result<Vec<Session>, String> {
|
pub fn session_list(state: State<'_, AppState>) -> Result<Vec<Session>, AppError> {
|
||||||
state.session_db.list_sessions()
|
state.session_db.list_sessions()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn session_save(state: State<'_, AppState>, session: Session) -> Result<(), String> {
|
pub fn session_save(state: State<'_, AppState>, session: Session) -> Result<(), AppError> {
|
||||||
state.session_db.save_session(&session)
|
state.session_db.save_session(&session)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn session_delete(state: State<'_, AppState>, id: String) -> Result<(), String> {
|
pub fn session_delete(state: State<'_, AppState>, id: String) -> Result<(), AppError> {
|
||||||
state.session_db.delete_session(&id)
|
state.session_db.delete_session(&id)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn session_update_title(state: State<'_, AppState>, id: String, title: String) -> Result<(), String> {
|
pub fn session_update_title(state: State<'_, AppState>, id: String, title: String) -> Result<(), AppError> {
|
||||||
state.session_db.update_title(&id, &title)
|
state.session_db.update_title(&id, &title)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn session_touch(state: State<'_, AppState>, id: String) -> Result<(), String> {
|
pub fn session_touch(state: State<'_, AppState>, id: String) -> Result<(), AppError> {
|
||||||
state.session_db.touch_session(&id)
|
state.session_db.touch_session(&id)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn session_update_group(state: State<'_, AppState>, id: String, group_name: String) -> Result<(), String> {
|
pub fn session_update_group(state: State<'_, AppState>, id: String, group_name: String) -> Result<(), AppError> {
|
||||||
state.session_db.update_group(&id, &group_name)
|
state.session_db.update_group(&id, &group_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Layout ---
|
// --- Layout ---
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn layout_save(state: State<'_, AppState>, layout: LayoutState) -> Result<(), String> {
|
pub fn layout_save(state: State<'_, AppState>, layout: LayoutState) -> Result<(), AppError> {
|
||||||
state.session_db.save_layout(&layout)
|
state.session_db.save_layout(&layout)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn layout_load(state: State<'_, AppState>) -> Result<LayoutState, String> {
|
pub fn layout_load(state: State<'_, AppState>) -> Result<LayoutState, AppError> {
|
||||||
state.session_db.load_layout()
|
state.session_db.load_layout()
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Settings ---
|
// --- Settings ---
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn settings_get(state: State<'_, AppState>, key: String) -> Result<Option<String>, String> {
|
pub fn settings_get(state: State<'_, AppState>, key: String) -> Result<Option<String>, AppError> {
|
||||||
state.session_db.get_setting(&key)
|
state.session_db.get_setting(&key)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn settings_set(state: State<'_, AppState>, key: String, value: String) -> Result<(), String> {
|
pub fn settings_set(state: State<'_, AppState>, key: String, value: String) -> Result<(), AppError> {
|
||||||
state.session_db.set_setting(&key, &value)
|
state.session_db.set_setting(&key, &value)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn settings_list(state: State<'_, AppState>) -> Result<Vec<(String, String)>, String> {
|
pub fn settings_list(state: State<'_, AppState>) -> Result<Vec<(String, String)>, AppError> {
|
||||||
state.session_db.get_all_settings()
|
state.session_db.get_all_settings()
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- SSH sessions ---
|
// --- SSH sessions ---
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn ssh_session_list(state: State<'_, AppState>) -> Result<Vec<SshSession>, String> {
|
pub fn ssh_session_list(state: State<'_, AppState>) -> Result<Vec<SshSession>, AppError> {
|
||||||
state.session_db.list_ssh_sessions()
|
state.session_db.list_ssh_sessions()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn ssh_session_save(state: State<'_, AppState>, session: SshSession) -> Result<(), String> {
|
pub fn ssh_session_save(state: State<'_, AppState>, session: SshSession) -> Result<(), AppError> {
|
||||||
state.session_db.save_ssh_session(&session)
|
state.session_db.save_ssh_session(&session)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn ssh_session_delete(state: State<'_, AppState>, id: String) -> Result<(), String> {
|
pub fn ssh_session_delete(state: State<'_, AppState>, id: String) -> Result<(), AppError> {
|
||||||
state.session_db.delete_ssh_session(&id)
|
state.session_db.delete_ssh_session(&id)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
use crate::error::AppError;
|
||||||
use crate::fs_watcher::FsWatcherStatus;
|
use crate::fs_watcher::FsWatcherStatus;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|
@ -8,8 +9,8 @@ pub fn file_watch(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
pane_id: String,
|
pane_id: String,
|
||||||
path: String,
|
path: String,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, AppError> {
|
||||||
state.file_watcher.watch(&app, &pane_id, &path)
|
state.file_watcher.watch(&app, &pane_id, &path).map_err(AppError::filesystem)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|
@ -18,8 +19,8 @@ pub fn file_unwatch(state: State<'_, AppState>, pane_id: String) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn file_read(state: State<'_, AppState>, path: String) -> Result<String, String> {
|
pub fn file_read(state: State<'_, AppState>, path: String) -> Result<String, AppError> {
|
||||||
state.file_watcher.read_file(&path)
|
state.file_watcher.read_file(&path).map_err(AppError::filesystem)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|
@ -28,8 +29,8 @@ pub fn fs_watch_project(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
project_id: String,
|
project_id: String,
|
||||||
cwd: String,
|
cwd: String,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), AppError> {
|
||||||
state.fs_watcher.watch_project(&app, &project_id, &cwd)
|
state.fs_watcher.watch_project(&app, &project_id, &cwd).map_err(AppError::filesystem)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|
|
||||||
222
src-tauri/src/error.rs
Normal file
222
src-tauri/src/error.rs
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
//! Typed application error enum.
|
||||||
|
//!
|
||||||
|
//! Replaces `Result<T, String>` across Tauri commands and backend modules.
|
||||||
|
//! Tauri 2.x requires `serde::Serialize` on command error types.
|
||||||
|
//! The custom `Serialize` impl produces `{ kind: "Database", detail: "..." }`
|
||||||
|
//! so the frontend receives structured, classifiable errors.
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
/// Structured error type for all Tauri commands and backend operations.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum AppError {
|
||||||
|
Database { detail: String },
|
||||||
|
Auth { detail: String },
|
||||||
|
Filesystem { detail: String },
|
||||||
|
Ipc { detail: String },
|
||||||
|
NotFound { detail: String },
|
||||||
|
Validation { detail: String },
|
||||||
|
Sidecar { detail: String },
|
||||||
|
Config { detail: String },
|
||||||
|
Network { detail: String },
|
||||||
|
Internal { detail: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for AppError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
AppError::Database { detail } => write!(f, "Database: {detail}"),
|
||||||
|
AppError::Auth { detail } => write!(f, "Auth: {detail}"),
|
||||||
|
AppError::Filesystem { detail } => write!(f, "Filesystem: {detail}"),
|
||||||
|
AppError::Ipc { detail } => write!(f, "IPC: {detail}"),
|
||||||
|
AppError::NotFound { detail } => write!(f, "NotFound: {detail}"),
|
||||||
|
AppError::Validation { detail } => write!(f, "Validation: {detail}"),
|
||||||
|
AppError::Sidecar { detail } => write!(f, "Sidecar: {detail}"),
|
||||||
|
AppError::Config { detail } => write!(f, "Config: {detail}"),
|
||||||
|
AppError::Network { detail } => write!(f, "Network: {detail}"),
|
||||||
|
AppError::Internal { detail } => write!(f, "Internal: {detail}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for AppError {}
|
||||||
|
|
||||||
|
/// Helper struct for structured JSON serialization.
|
||||||
|
/// Produces `{ "kind": "Database", "detail": "message" }`.
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ErrorPayload<'a> {
|
||||||
|
kind: &'a str,
|
||||||
|
detail: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for AppError {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::ser::Serializer,
|
||||||
|
{
|
||||||
|
let (kind, detail) = match self {
|
||||||
|
AppError::Database { detail } => ("Database", detail.as_str()),
|
||||||
|
AppError::Auth { detail } => ("Auth", detail.as_str()),
|
||||||
|
AppError::Filesystem { detail } => ("Filesystem", detail.as_str()),
|
||||||
|
AppError::Ipc { detail } => ("IPC", detail.as_str()),
|
||||||
|
AppError::NotFound { detail } => ("NotFound", detail.as_str()),
|
||||||
|
AppError::Validation { detail } => ("Validation", detail.as_str()),
|
||||||
|
AppError::Sidecar { detail } => ("Sidecar", detail.as_str()),
|
||||||
|
AppError::Config { detail } => ("Config", detail.as_str()),
|
||||||
|
AppError::Network { detail } => ("Network", detail.as_str()),
|
||||||
|
AppError::Internal { detail } => ("Internal", detail.as_str()),
|
||||||
|
};
|
||||||
|
ErrorPayload { kind, detail }.serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Convenience conversions ---
|
||||||
|
|
||||||
|
impl From<rusqlite::Error> for AppError {
|
||||||
|
fn from(e: rusqlite::Error) -> Self {
|
||||||
|
AppError::Database {
|
||||||
|
detail: e.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for AppError {
|
||||||
|
fn from(e: std::io::Error) -> Self {
|
||||||
|
AppError::Filesystem {
|
||||||
|
detail: e.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for AppError {
|
||||||
|
fn from(e: serde_json::Error) -> Self {
|
||||||
|
AppError::Validation {
|
||||||
|
detail: e.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert from legacy `String` errors (used at the boundary between
|
||||||
|
/// agor-core functions that still return `Result<T, String>` and
|
||||||
|
/// Tauri commands that now return `Result<T, AppError>`).
|
||||||
|
impl From<String> for AppError {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
AppError::Internal { detail: s }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for AppError {
|
||||||
|
fn from(s: &str) -> Self {
|
||||||
|
AppError::Internal {
|
||||||
|
detail: s.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience constructors for common error patterns.
|
||||||
|
impl AppError {
|
||||||
|
pub fn database(detail: impl Into<String>) -> Self {
|
||||||
|
AppError::Database {
|
||||||
|
detail: detail.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn auth(detail: impl Into<String>) -> Self {
|
||||||
|
AppError::Auth {
|
||||||
|
detail: detail.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn filesystem(detail: impl Into<String>) -> Self {
|
||||||
|
AppError::Filesystem {
|
||||||
|
detail: detail.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn not_found(detail: impl Into<String>) -> Self {
|
||||||
|
AppError::NotFound {
|
||||||
|
detail: detail.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validation(detail: impl Into<String>) -> Self {
|
||||||
|
AppError::Validation {
|
||||||
|
detail: detail.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sidecar(detail: impl Into<String>) -> Self {
|
||||||
|
AppError::Sidecar {
|
||||||
|
detail: detail.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn config(detail: impl Into<String>) -> Self {
|
||||||
|
AppError::Config {
|
||||||
|
detail: detail.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn network(detail: impl Into<String>) -> Self {
|
||||||
|
AppError::Network {
|
||||||
|
detail: detail.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn internal(detail: impl Into<String>) -> Self {
|
||||||
|
AppError::Internal {
|
||||||
|
detail: detail.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_display() {
|
||||||
|
let e = AppError::database("connection refused");
|
||||||
|
assert_eq!(e.to_string(), "Database: connection refused");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_produces_structured_json() {
|
||||||
|
let e = AppError::Database {
|
||||||
|
detail: "table not found".to_string(),
|
||||||
|
};
|
||||||
|
let json = serde_json::to_value(&e).unwrap();
|
||||||
|
assert_eq!(json["kind"], "Database");
|
||||||
|
assert_eq!(json["detail"], "table not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_rusqlite_error() {
|
||||||
|
let e: AppError = rusqlite::Error::SqliteFailure(
|
||||||
|
rusqlite::ffi::Error::new(1),
|
||||||
|
Some("test".to_string()),
|
||||||
|
)
|
||||||
|
.into();
|
||||||
|
assert!(matches!(e, AppError::Database { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_io_error() {
|
||||||
|
let e: AppError = std::io::Error::new(std::io::ErrorKind::NotFound, "gone").into();
|
||||||
|
assert!(matches!(e, AppError::Filesystem { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_string() {
|
||||||
|
let e: AppError = "something went wrong".into();
|
||||||
|
assert!(matches!(e, AppError::Internal { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_serde_json_error() {
|
||||||
|
let e: AppError = serde_json::from_str::<serde_json::Value>("not json")
|
||||||
|
.unwrap_err()
|
||||||
|
.into();
|
||||||
|
assert!(matches!(e, AppError::Validation { .. }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
mod btmsg;
|
mod btmsg;
|
||||||
mod bttask;
|
mod bttask;
|
||||||
mod commands;
|
mod commands;
|
||||||
|
pub mod error;
|
||||||
mod ctx;
|
mod ctx;
|
||||||
mod event_sink;
|
mod event_sink;
|
||||||
mod fs_watcher;
|
mod fs_watcher;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue