Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions cli/src/cli_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ pub enum Commands {

#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
format: OutputFormat,

#[command(subcommand)]
subcommand: Option<DoctorSubcommand>,
},

#[command(about = HOOKS_CLAP_ABOUT, hide = !HOOKS_SHOW_IN_TOP_LEVEL_HELP)]
Expand Down Expand Up @@ -209,6 +212,15 @@ pub enum Commands {
},
}

#[derive(Subcommand, Debug, Clone, PartialEq, Eq)]
pub enum DoctorSubcommand {
#[command(about = "List registered Agent Trace checkouts and databases")]
Dbs {
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
format: OutputFormat,
},
}

#[derive(Subcommand, Debug, Clone, PartialEq, Eq)]
pub enum AuthSubcommand {
#[command(about = "Start login flow and store credentials")]
Expand Down
1 change: 0 additions & 1 deletion cli/src/generated_migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,3 @@ pub static AUTH_MIGRATIONS: &[(&str, &str)] = &[
("001_create_auth_tokens", include_str!("../migrations/auth/001_create_auth_tokens.sql")),
("002_create_auth_credentials_updated_at_trigger", include_str!("../migrations/auth/002_create_auth_credentials_updated_at_trigger.sql")),
];

151 changes: 138 additions & 13 deletions cli/src/services/agent_trace_db/lifecycle.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};

use crate::app::HasRepoRoot;
use crate::services::checkout;
use crate::services::db::{bootstrap_db_parent, collect_db_path_health, DbSpec};
use crate::services::default_paths::agent_trace_db_path;
use crate::services::default_paths::{agent_trace_db_path, agent_trace_db_path_for_checkout};
use crate::services::lifecycle::{
FixOutcome, FixResultRecord, HealthCategory, HealthFixability, HealthProblem,
HealthProblemKind, HealthSeverity, LifecycleProviderId, ServiceLifecycle, SetupOutcome,
Expand All @@ -18,11 +20,11 @@ impl ServiceLifecycle for AgentTraceDbLifecycle {
LifecycleProviderId::AgentTraceDb
}

fn diagnose<C: HasRepoRoot>(&self, _ctx: &C) -> Vec<HealthProblem> {
diagnose_agent_trace_db_health()
fn diagnose<C: HasRepoRoot>(&self, ctx: &C) -> Vec<HealthProblem> {
diagnose_agent_trace_db_health(ctx.repo_root())
}

fn fix<C: HasRepoRoot>(&self, _ctx: &C, problems: &[HealthProblem]) -> Vec<FixResultRecord> {
fn fix<C: HasRepoRoot>(&self, ctx: &C, problems: &[HealthProblem]) -> Vec<FixResultRecord> {
let should_bootstrap_parent = problems.iter().any(|problem| {
problem.category == HealthCategory::GlobalState
&& problem.fixability == HealthFixability::AutoFixable
Expand All @@ -31,7 +33,7 @@ impl ServiceLifecycle for AgentTraceDbLifecycle {
return Vec::new();
}

match bootstrap_agent_trace_db_parent() {
match bootstrap_agent_trace_db_parent(ctx.repo_root()) {
Ok(parent) => vec![FixResultRecord {
category: HealthCategory::GlobalState,
outcome: FixOutcome::Fixed,
Expand All @@ -50,17 +52,83 @@ impl ServiceLifecycle for AgentTraceDbLifecycle {
}
}

fn setup<C: HasRepoRoot>(&self, _ctx: &C) -> Result<SetupOutcome> {
AgentTraceDb::new()
.context("Agent trace DB lifecycle setup failed while initializing agent trace DB")?;
Ok(SetupOutcome::default())
fn setup<C: HasRepoRoot>(&self, ctx: &C) -> Result<SetupOutcome> {
let checkout_setup = match ctx.repo_root() {
Some(repo_root) => {
let checkout_id = setup_checkout_identity(repo_root).context(
"Agent trace DB lifecycle setup failed while resolving checkout identity",
)?;
Some(initialize_checkout_agent_trace_db(&checkout_id).context(
"Agent trace DB lifecycle setup failed while initializing checkout database",
)?)
}
None => None,
};

Ok(SetupOutcome {
messages: checkout_setup
.iter()
.map(format_checkout_identity_setup_message)
.collect(),
..SetupOutcome::default()
})
}
}

pub fn diagnose_agent_trace_db_health() -> Vec<HealthProblem> {
#[derive(Clone, Debug, Eq, PartialEq)]
struct CheckoutDatabaseSetup {
checkout_id: String,
database_path: PathBuf,
}

fn setup_checkout_identity(repo_root: &std::path::Path) -> Result<String> {
let git_dir = checkout::resolve_git_dir(repo_root).with_context(|| {
format!(
"failed to resolve git directory for checkout identity from '{}'",
repo_root.display()
)
})?;
let checkout_id = checkout::get_or_create_checkout_id(&git_dir).with_context(|| {
format!(
"failed to get or create checkout identity under '{}'",
git_dir.display()
)
})?;

Ok(checkout_id)
}

fn initialize_checkout_agent_trace_db(checkout_id: &str) -> Result<CheckoutDatabaseSetup> {
let db_path = agent_trace_db_path_for_checkout(checkout_id).with_context(|| {
format!("failed to resolve Agent Trace DB path for checkout ID {checkout_id}")
})?;

AgentTraceDb::open_at(&db_path).with_context(|| {
format!(
"failed to initialize Agent Trace DB for checkout {} at '{}'",
checkout_id,
db_path.display()
)
})?;

Ok(CheckoutDatabaseSetup {
checkout_id: checkout_id.to_string(),
database_path: db_path,
})
}

fn format_checkout_identity_setup_message(setup: &CheckoutDatabaseSetup) -> String {
format!(
"Agent Trace checkout identity: {}\nAgent Trace database initialized at '{}'.",
setup.checkout_id,
setup.database_path.display()
)
}

pub fn diagnose_agent_trace_db_health(repo_root: Option<&Path>) -> Vec<HealthProblem> {
let mut problems = Vec::new();

let db_path = match agent_trace_db_path() {
let db_path = match resolve_lifecycle_agent_trace_db_path(repo_root) {
Ok(path) => path,
Err(error) => {
problems.push(HealthProblem {
Expand All @@ -81,10 +149,67 @@ pub fn diagnose_agent_trace_db_health() -> Vec<HealthProblem> {
&db_path,
&mut problems,
);

if db_path.exists() && db_path.is_file() {
match AgentTraceDb::open_for_hooks_without_migrations_at(&db_path) {
Ok(db) => {
if let Err(error) = db.ensure_schema_ready_for_hooks() {
problems.push(HealthProblem {
kind: HealthProblemKind::AgentTraceDbSchemaNotReady,
category: HealthCategory::GlobalState,
severity: HealthSeverity::Error,
fixability: HealthFixability::ManualOnly,
summary: format!(
"Agent Trace database schema at '{}' is not ready: {error}",
db_path.display()
),
remediation: String::from(
"Re-run 'sce setup' to apply missing migrations, or inspect the database file for corruption.",
),
next_action: "manual_steps",
});
}
}
Err(error) => {
problems.push(HealthProblem {
kind: HealthProblemKind::AgentTraceDbConnectionFailed,
category: HealthCategory::GlobalState,
severity: HealthSeverity::Error,
fixability: HealthFixability::ManualOnly,
summary: format!(
"Unable to open checkout Agent Trace database at '{}': {error}",
db_path.display()
),
remediation: String::from(
"Verify file permissions and ensure the file is a valid SQLite database. Re-run 'sce setup' to recreate it if needed.",
),
next_action: "manual_steps",
});
}
}
}

problems
}

fn bootstrap_agent_trace_db_parent() -> Result<std::path::PathBuf> {
let db_path = agent_trace_db_path().context("failed to resolve agent trace DB path")?;
fn bootstrap_agent_trace_db_parent(repo_root: Option<&Path>) -> Result<PathBuf> {
let db_path = resolve_lifecycle_agent_trace_db_path(repo_root)
.context("failed to resolve agent trace DB path")?;
bootstrap_db_parent(<AgentTraceDbSpec as DbSpec>::db_name(), &db_path)
}

fn resolve_lifecycle_agent_trace_db_path(repo_root: Option<&Path>) -> Result<PathBuf> {
if let Some(repo_root) = repo_root {
let git_dir = checkout::resolve_git_dir(repo_root).with_context(|| {
format!(
"failed to resolve git directory for agent trace DB health from '{}'",
repo_root.display()
)
})?;
if let Some(checkout_id) = checkout::read_checkout_id(&git_dir)? {
return agent_trace_db_path_for_checkout(&checkout_id);
}
}

agent_trace_db_path()
}
15 changes: 15 additions & 0 deletions cli/src/services/agent_trace_db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,20 @@ impl DbSpec for AgentTraceDbSpec {
/// Agent trace Turso database adapter.
pub type AgentTraceDb = TursoDb<AgentTraceDbSpec>;

impl AgentTraceDb {
/// Open or create an Agent Trace database at an explicit path and run all
/// embedded migrations.
pub fn open_at(path: impl AsRef<std::path::Path>) -> Result<Self> {
TursoDb::<AgentTraceDbSpec>::new_at(path)
}

/// Open or create an Agent Trace database at an explicit path without
/// running migrations.
pub fn open_for_hooks_without_migrations_at(path: impl AsRef<std::path::Path>) -> Result<Self> {
TursoDb::<AgentTraceDbSpec>::open_without_migrations_at(path)
}
}

/// Diff trace payload to persist in the agent trace database.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DiffTraceInsert<'a> {
Expand Down Expand Up @@ -288,6 +302,7 @@ impl AgentTraceDb {
/// Setup/lifecycle initialization must continue to use [`AgentTraceDb::new`]
/// so schema migrations remain explicitly owned by setup flows. Hook callers
/// must verify schema readiness before reading or writing through this DB.
#[allow(dead_code)]
pub fn open_for_hooks_without_migrations() -> Result<Self> {
TursoDb::<AgentTraceDbSpec>::open_without_migrations()
}
Expand Down
2 changes: 1 addition & 1 deletion cli/src/services/auth_command/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ pub struct AuthCommand {
impl AuthCommand {
pub fn execute<C>(&self, _context: &C) -> Result<String, ClassifiedError> {
auth_command::run_auth_subcommand(self.request)
.map_err(|error| ClassifiedError::runtime(error.to_string()))
.map_err(|error| ClassifiedError::runtime(format!("{error:#}")))
}
}
Loading