use std::sync::Arc;
use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::Json;
use serde::{Deserialize, Serialize};
use crate::routes::{paginate, PageParams};
use crate::server::AppState;
#[derive(Debug, Serialize, Clone)]
pub(crate) struct ProgramSummary {
name: String,
version: String,
description: String,
author: String,
enabled: bool,
tools_count: usize,
has_skill_content: bool,
}
pub(crate) async fn handle_programs_list(
state: State<Arc<AppState>>,
Query(params): Query<PageParams>,
) -> Json<serde_json::Value> {
let programs = state.kernel.extensions.list_programs().await;
let summaries: Vec<ProgramSummary> = programs
.into_iter()
.map(|p| ProgramSummary {
name: p.name,
version: p.version,
description: p.description,
author: p.author,
enabled: false, tools_count: p.tools.len(),
has_skill_content: false, })
.collect();
Json(paginate(&summaries, ¶ms))
}
pub(crate) async fn handle_program_get(
state: State<Arc<AppState>>,
Path(name): Path<String>,
) -> Result<Json<serde_json::Value>, StatusCode> {
match state.kernel.extensions.get_program(&name).await {
Some(program) => Ok(Json(serde_json::json!({
"name": program.meta.name,
"version": program.meta.version,
"description": program.meta.description,
"author": program.meta.author,
"enabled": program.enabled,
"tools": program.meta.tools,
"skill_content": program.skill_content,
"path": program.path.to_string_lossy(),
}))),
None => Err(StatusCode::NOT_FOUND),
}
}
#[derive(Debug, Deserialize)]
pub(crate) struct ProgramInstallRequest {
path: String,
}
pub(crate) async fn handle_program_install(
state: State<Arc<AppState>>,
Json(body): Json<ProgramInstallRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
const MAX_SOURCE_LENGTH: usize = 8192;
if body.path.len() > MAX_SOURCE_LENGTH {
return Err((
StatusCode::PAYLOAD_TOO_LARGE,
format!(
"Source URL too long: {} bytes exceeds limit of {} bytes",
body.path.len(),
MAX_SOURCE_LENGTH,
),
));
}
use oxios_kernel::InstallSource;
let source = if body.path.ends_with(".git") || body.path.starts_with("git@") {
InstallSource::Git {
url: body.path.clone(),
branch: None,
}
} else if body.path.starts_with("http://") || body.path.starts_with("https://") {
InstallSource::Tarball {
url: body.path.clone(),
}
} else {
return Err((
StatusCode::BAD_REQUEST,
"Local path installation not allowed via API. Use git URL or tarball URL.".into(),
));
};
state
.kernel
.extensions
.install_program(source)
.await
.map(|program| {
tracing::info!(program = %program.meta.name, "Program installed via API");
Json(serde_json::json!({
"status": "installed",
"name": program.meta.name,
"version": program.meta.version,
}))
})
.map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))
}
pub(crate) async fn handle_program_uninstall(
state: State<Arc<AppState>>,
Path(name): Path<String>,
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
state
.kernel
.extensions
.uninstall_program(&name)
.await
.map(|_| {
tracing::info!(program = %name, "Program uninstalled via API");
Json(serde_json::json!({"status": "uninstalled", "name": name}))
})
.map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))
}
pub(crate) async fn handle_program_enable(
state: State<Arc<AppState>>,
Path(name): Path<String>,
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
state
.kernel
.extensions
.enable_program(&name)
.await
.map(|_| Json(serde_json::json!({"status": "enabled", "name": name})))
.map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))
}
pub(crate) async fn handle_program_disable(
state: State<Arc<AppState>>,
Path(name): Path<String>,
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
state
.kernel
.extensions
.disable_program(&name)
.await
.map(|_| Json(serde_json::json!({"status": "disabled", "name": name})))
.map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))
}
pub(crate) async fn handle_program_host_requirements(
state: State<Arc<AppState>>,
Path(name): Path<String>,
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
state
.kernel
.extensions
.check_host_requirements(&name)
.await
.map(|check| serde_json::to_value(&check).map(Json))
.map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
}
#[derive(Debug, Serialize, Clone)]
pub(crate) struct HostToolsStatusResponse {
all_required_present: bool,
missing_required: Vec<String>,
optional_available: std::collections::HashMap<String, bool>,
}
pub(crate) async fn handle_host_tools_check(
state: State<Arc<AppState>>,
) -> Json<HostToolsStatusResponse> {
let status = state.kernel.extensions.check_host_tools();
Json(HostToolsStatusResponse {
all_required_present: status.all_required_present,
missing_required: status.missing_required,
optional_available: status.optional_available,
})
}