refactor(#2464): rename hive-ag3nt crate to hive-agent, collapse lib into main

This commit is contained in:
damocles 2026-07-15 01:18:22 +02:00 committed by mara
commit 3f1643c594
57 changed files with 101 additions and 130 deletions

View file

@ -0,0 +1,117 @@
//! Login / logout flow handlers (`/login/*`, `/api/logout`).
use std::sync::Arc;
use axum::{
Form,
extract::State,
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::Deserialize;
use crate::login::LoginState;
use crate::login_session::{LoginSession, drop_if_finished};
use super::{AppState, error_response};
pub(super) async fn post_login_start(State(state): State<AppState>) -> Response {
drop_if_finished(&state.session);
{
let guard = state.session.lock().unwrap();
if guard.is_some() {
return (axum::http::StatusCode::OK, "ok").into_response();
}
}
match LoginSession::start() {
Ok(session) => {
*state.session.lock().unwrap() = Some(Arc::new(session));
// Flip status from needs_login_idle → needs_login_in_progress
// so the web UI's badge + polling kick in (polling is still
// the right tool for the streaming session output during
// the login flow itself; events drop the poll for
// *everything else*).
state.bus.emit_status("needs_login_in_progress");
(axum::http::StatusCode::OK, "ok").into_response()
}
Err(e) => error_response(
StatusCode::INTERNAL_SERVER_ERROR,
&format!("login start failed: {e:#}"),
),
}
}
#[derive(Deserialize)]
pub(super) struct CodeForm {
code: String,
}
pub(super) async fn post_login_code(
State(state): State<AppState>,
Form(form): Form<CodeForm>,
) -> Response {
let session = state.session.lock().unwrap().clone();
let Some(session) = session else {
return error_response(StatusCode::CONFLICT, "no login session running");
};
if let Err(e) = session.submit_code(&form.code).await {
return error_response(
StatusCode::INTERNAL_SERVER_ERROR,
&format!("submit code failed: {e:#}"),
);
}
(axum::http::StatusCode::OK, "ok").into_response()
}
pub(super) async fn post_login_cancel(State(state): State<AppState>) -> Response {
let session = state.session.lock().unwrap().take();
if let Some(session) = session {
session.close_stdin().await;
session.kill();
}
// Back to needs_login_idle (LoginState unchanged, session gone).
state.bus.emit_status("needs_login_idle");
(axum::http::StatusCode::OK, "ok").into_response()
}
/// Operator-driven `/logout`: SIGINT claude, delete the credential
/// files (via [`crate::login::clear_session`]), flip `LoginState::NeedsLogin`.
/// The turn loop's next iteration parks into `wait_for_login` which
/// resumes when a fresh credentials file appears via `/login/code`.
/// Always returns 200 with a body describing what happened. See
/// [`docs/web-ui/agent.md::Per-agent endpoints`](../../../docs/web-ui/agent.md)
/// (the `/api/logout` bullet) for the three-step rationale +
/// preservation invariants.
pub(super) async fn post_logout(State(state): State<AppState>) -> Response {
// Step 1: SIGINT claude (best-effort, matches `post_cancel_turn`).
let _ = super::sigint_claude().await;
// Step 2: delete OAuth credential files only — login::clear_session owns
// the file set and preserves session-history files alongside them.
let dir = crate::paths::claude_dir();
let cleared = crate::login::clear_session(&dir).await;
let wipe_summary = if cleared.wiped.is_empty() {
"no credential files present (already logged out)".to_owned()
} else {
format!("wiped {}", cleared.wiped.join(", "))
};
let warn_suffix = if cleared.warnings.is_empty() {
String::new()
} else {
format!(" (warnings: {})", cleared.warnings.join("; "))
};
// Step 3: flip LoginState + emit Note. Turn loop sees the flip on
// its next iteration and parks into wait_for_login.
*state.login.lock().unwrap() = LoginState::NeedsLogin;
state.bus.emit(crate::events::LiveEvent::Note {
text: format!(
"operator: /logout — {wipe_summary} in {}{warn_suffix}",
dir.display()
),
});
state.bus.emit_status("needs_login_idle");
(
axum::http::StatusCode::OK,
format!("ok: {wipe_summary} in {}{warn_suffix}", dir.display()),
)
.into_response()
}