use std::path::PathBuf;
use std::sync::Arc;
use axum::{
Json,
extract::{Path, Query, State},
http::StatusCode,
};
use crate::api::types::{
ApplyConfigResponse, CheckpointsResponse, ClaudeConfigResponse, CreateCheckpointResponse,
DeleteCheckpointResponse, DeployProfileResponse, ProfilesResponse, RestartResponse,
RestoreResponse,
};
use crate::state::DaemonState;
#[derive(serde::Deserialize)]
pub struct ClaudeConfigQuery {
pub project: PathBuf,
}
#[utoipa::path(
get,
path = "/claude-config",
tag = "claude-config",
params(("project" = String, Query, description = "Project directory")),
responses((status = 200, description = "Analyzed config plus recommendations"))
)]
pub async fn get_claude_config(
State(_state): State<Arc<DaemonState>>,
Query(query): Query<ClaudeConfigQuery>,
) -> Json<ClaudeConfigResponse> {
use trusty_mpm_core::claude_config::ClaudeConfigReader;
let paths = ClaudeConfigReader::paths_for_project(&query.project);
let config = crate::claude_config::ClaudeConfigAnalyzer::read_config(&paths);
let recommendations = crate::claude_config::ClaudeConfigAnalyzer::analyze(&config);
Json(ClaudeConfigResponse {
config,
recommendations,
})
}
#[derive(serde::Deserialize, utoipa::ToSchema)]
pub struct ApplyConfigRequest {
#[schema(value_type = String)]
pub project: PathBuf,
pub recommendation_id: String,
}
#[utoipa::path(
post,
path = "/claude-config/apply",
tag = "claude-config",
request_body = ApplyConfigRequest,
responses(
(status = 200, description = "Recommendation applied; returns checkpoint id"),
(status = 404, description = "No recommendation with that id"),
(status = 500, description = "Applying the recommendation failed"),
)
)]
pub async fn apply_claude_config(
State(_state): State<Arc<DaemonState>>,
Json(body): Json<ApplyConfigRequest>,
) -> Result<Json<ApplyConfigResponse>, StatusCode> {
use trusty_mpm_core::claude_config::ClaudeConfigReader;
let paths = ClaudeConfigReader::paths_for_project(&body.project);
let config = crate::claude_config::ClaudeConfigAnalyzer::read_config(&paths);
let recommendations = crate::claude_config::ClaudeConfigAnalyzer::analyze(&config);
let rec = recommendations
.iter()
.find(|r| r.id == body.recommendation_id)
.ok_or(StatusCode::NOT_FOUND)?;
let checkpoint_id = crate::claude_config::ClaudeConfigAnalyzer::apply_recommendation(
rec,
&paths,
&body.project,
)
.map_err(|e| {
tracing::warn!("applying recommendation {} failed: {e}", rec.id);
StatusCode::INTERNAL_SERVER_ERROR
})?;
Ok(Json(ApplyConfigResponse {
applied: true,
recommendation_id: body.recommendation_id,
checkpoint_id,
}))
}
#[derive(serde::Deserialize)]
pub struct CheckpointQuery {
pub project: PathBuf,
}
#[utoipa::path(
get,
path = "/claude-config/checkpoints",
tag = "claude-config",
params(("project" = String, Query, description = "Project directory")),
responses((status = 200, description = "Config checkpoints, newest first"))
)]
pub async fn list_checkpoints(
State(_state): State<Arc<DaemonState>>,
Query(query): Query<CheckpointQuery>,
) -> Json<CheckpointsResponse> {
let checkpoints = crate::claude_config::ConfigCheckpointer::list(&query.project)
.unwrap_or_else(|e| {
tracing::warn!("listing checkpoints failed: {e}");
Vec::new()
});
Json(CheckpointsResponse { checkpoints })
}
#[derive(serde::Deserialize, utoipa::ToSchema)]
pub struct CreateCheckpointRequest {
#[schema(value_type = String)]
pub project: PathBuf,
#[serde(default)]
pub label: Option<String>,
}
#[utoipa::path(
post,
path = "/claude-config/checkpoints",
tag = "claude-config",
request_body = CreateCheckpointRequest,
responses(
(status = 200, description = "Checkpoint created; returns its id"),
(status = 500, description = "Creating the checkpoint failed"),
)
)]
pub async fn create_checkpoint(
State(_state): State<Arc<DaemonState>>,
Json(body): Json<CreateCheckpointRequest>,
) -> Result<Json<CreateCheckpointResponse>, StatusCode> {
use trusty_mpm_core::claude_config::ClaudeConfigReader;
let paths = ClaudeConfigReader::paths_for_project(&body.project);
let id = crate::claude_config::ConfigCheckpointer::create(
&paths,
&body.project,
body.label.as_deref(),
)
.map_err(|e| {
tracing::warn!("creating checkpoint failed: {e}");
StatusCode::INTERNAL_SERVER_ERROR
})?;
Ok(Json(CreateCheckpointResponse { id }))
}
#[derive(serde::Deserialize, utoipa::ToSchema)]
pub struct RestoreRequest {
#[schema(value_type = String)]
pub project: PathBuf,
pub checkpoint_id: String,
}
#[utoipa::path(
post,
path = "/claude-config/restore",
tag = "claude-config",
request_body = RestoreRequest,
responses(
(status = 200, description = "Config restored from the checkpoint"),
(status = 500, description = "Checkpoint missing or restore failed"),
)
)]
pub async fn restore_checkpoint(
State(_state): State<Arc<DaemonState>>,
Json(body): Json<RestoreRequest>,
) -> Result<Json<RestoreResponse>, StatusCode> {
crate::claude_config::ConfigCheckpointer::restore(&body.project, &body.checkpoint_id).map_err(
|e| {
tracing::warn!("restoring checkpoint {} failed: {e}", body.checkpoint_id);
StatusCode::INTERNAL_SERVER_ERROR
},
)?;
Ok(Json(RestoreResponse {
restored: true,
checkpoint_id: body.checkpoint_id,
}))
}
#[utoipa::path(
delete,
path = "/claude-config/checkpoints/{id}",
tag = "claude-config",
params(
("id" = String, Path, description = "Checkpoint id"),
("project" = String, Query, description = "Project directory"),
),
responses(
(status = 200, description = "Checkpoint deleted"),
(status = 404, description = "No checkpoint with that id"),
)
)]
pub async fn delete_checkpoint(
State(_state): State<Arc<DaemonState>>,
Path(id): Path<String>,
Query(query): Query<CheckpointQuery>,
) -> Result<Json<DeleteCheckpointResponse>, StatusCode> {
crate::claude_config::ConfigCheckpointer::delete(&query.project, &id).map_err(|e| {
tracing::warn!("deleting checkpoint {id} failed: {e}");
StatusCode::NOT_FOUND
})?;
Ok(Json(DeleteCheckpointResponse { deleted: id }))
}
#[utoipa::path(
get,
path = "/claude-config/profiles",
tag = "claude-config",
responses((status = 200, description = "Built-in deployment profiles"))
)]
pub async fn list_profiles(State(_state): State<Arc<DaemonState>>) -> Json<ProfilesResponse> {
let profiles = crate::claude_config::ProfileDeployer::builtin_profiles();
Json(ProfilesResponse { profiles })
}
#[derive(serde::Deserialize, utoipa::ToSchema)]
pub struct DeployProfileRequest {
#[schema(value_type = String)]
pub project: PathBuf,
pub profile_name: String,
#[serde(default)]
pub target: Option<trusty_mpm_core::claude_config::DeployTarget>,
}
#[utoipa::path(
post,
path = "/claude-config/deploy",
tag = "claude-config",
request_body = DeployProfileRequest,
responses(
(status = 200, description = "Profile deployed; returns checkpoint id"),
(status = 404, description = "No built-in profile with that name"),
(status = 500, description = "Deploying the profile failed"),
)
)]
pub async fn deploy_profile(
State(_state): State<Arc<DaemonState>>,
Json(body): Json<DeployProfileRequest>,
) -> Result<Json<DeployProfileResponse>, StatusCode> {
use trusty_mpm_core::claude_config::ClaudeConfigReader;
let mut profile = crate::claude_config::ProfileDeployer::builtin_profiles()
.into_iter()
.find(|p| p.name == body.profile_name)
.ok_or(StatusCode::NOT_FOUND)?;
if let Some(target) = body.target {
profile.target = target;
}
let paths = ClaudeConfigReader::paths_for_project(&body.project);
let checkpoint_id =
crate::claude_config::ProfileDeployer::deploy(&profile, &paths, &body.project).map_err(
|e| {
tracing::warn!("deploying profile {} failed: {e}", body.profile_name);
StatusCode::INTERNAL_SERVER_ERROR
},
)?;
Ok(Json(DeployProfileResponse {
deployed: body.profile_name,
checkpoint_id,
}))
}
#[derive(serde::Deserialize, utoipa::ToSchema)]
pub struct RestartRequest {
pub tmux_session: String,
}
#[utoipa::path(
post,
path = "/claude-config/restart",
tag = "claude-config",
request_body = RestartRequest,
responses(
(status = 200, description = "Restart command sent"),
(status = 500, description = "tmux unavailable or restart failed"),
)
)]
pub async fn restart_claude_code(
State(_state): State<Arc<DaemonState>>,
Json(body): Json<RestartRequest>,
) -> Result<Json<RestartResponse>, StatusCode> {
crate::claude_config::ClaudeCodeRestarter::restart_in_session(&body.tmux_session).map_err(
|e| {
tracing::warn!("restart in {} failed: {e}", body.tmux_session);
StatusCode::INTERNAL_SERVER_ERROR
},
)?;
Ok(Json(RestartResponse {
restarted: body.tmux_session,
}))
}