use std::error::Error;
use std::io;
use std::path::Path;
use frigg::settings::FriggConfig;
use frigg::storage::Storage;
use frigg::mcp::FriggMcpServer;
use super::super::storage_auto_heal::initialize_storage_with_auto_repair;
use super::{CliPreciseGenerationCounters, precise_counter_fields, run_cli_precise_generation};
use crate::cli_runtime::storage_paths::{
ensure_storage_db_path_for_write, resolve_storage_db_path,
};
use crate::cli_runtime::{CliOutput, OutputLevel, field, format_output_event_line, reported_error};
#[derive(Debug, Clone, Copy)]
pub(crate) enum StorageMaintenanceCommand {
RepairSemanticVectorStore,
Prune { keep_manifest_snapshots: usize },
}
fn storage_failure_next_step(command_name: &str, error: &str, db_path: &Path) -> String {
if error.contains("storage db file is missing") {
return format!(
"run `frigg init` or `frigg index` to create current storage at {}",
db_path.display()
);
}
if error.contains("exists but is not a file") {
return format!(
"move or delete {}, then run `frigg init` or `frigg index`",
db_path.display()
);
}
if error.contains("storage schema is uninitialized") {
return format!(
"run `frigg init` or `frigg index`; if {} is not a Frigg database, delete it first",
db_path.display()
);
}
if error.contains("legacy non-sqlite-vec schema")
|| error.contains("vector table schema mismatch")
|| error.contains("missing vector table")
{
if command_name == "repair-storage" {
return format!(
"`frigg repair-storage` could not rebuild vector storage; delete {} and run `frigg index`",
db_path.display()
);
}
return format!(
"rerun `frigg init` or `frigg index`; if repair fails, delete {} and run `frigg index`",
db_path.display()
);
}
if error.contains("storage schema is incompatible")
|| error.contains("schema version mismatch")
|| error.contains("missing required table")
{
return format!(
"delete {} and run `frigg index` to rebuild Frigg's local index state",
db_path.display()
);
}
if error.contains("semantic_vector_partition_in_sync") {
if command_name == "repair-storage" {
return format!(
"`frigg repair-storage` could not restore invariants; delete {} and run `frigg index`",
db_path.display()
);
}
return "rerun `frigg init` or `frigg index`; if it still fails, delete the storage DB and run `frigg index`"
.to_owned();
}
if error.contains("readonly")
|| error.contains("read-only")
|| error.contains("permission")
|| error.contains("Permission")
{
return format!(
"check write permissions for {} and its parent directory, then rerun `{command_name}`",
db_path.display()
);
}
format!(
"rerun `frigg --verbose {command_name}`; if the error persists, delete {db_path} and run `frigg index`",
db_path = db_path.display()
)
}
pub(crate) fn report_storage_failure(
output: &CliOutput,
command_name: &str,
repositories_len: usize,
repository_id: &str,
root: &Path,
db_path: &Path,
err: &dyn Error,
) -> io::Result<()> {
let error = err.to_string();
output.error_event(
command_name,
"failed",
&[
field("status", "failed"),
field("repos", repositories_len),
field("repo", repository_id),
field("db", db_path.display()),
field(
"next",
storage_failure_next_step(command_name, &error, db_path),
),
field("error", error),
],
Some(&root.display().to_string()),
)
}
#[cfg(test)]
pub(crate) fn run_storage_init_command(config: &FriggConfig) -> Result<(), Box<dyn Error>> {
run_storage_init_command_with_output(config, &CliOutput::normal())
}
pub(crate) fn run_storage_init_command_with_output(
config: &FriggConfig,
output: &CliOutput,
) -> Result<(), Box<dyn Error>> {
let repositories = config.repositories();
let command_name = "init";
let precise_server = FriggMcpServer::new(config.clone());
let mut total_precise = CliPreciseGenerationCounters::default();
output.progress_event(
OutputLevel::Info,
command_name,
"start",
&[
field("status", "starting"),
field("repos", repositories.len()),
],
None,
)?;
for repo in &repositories {
let root = match config.root_by_repository_id(&repo.repository_id.0) {
Some(root) => root,
None => {
let message = format_output_event_line(
OutputLevel::Error,
command_name,
"failed",
&[
field("status", "failed"),
field("repo", &repo.repository_id.0),
field("error", "workspace root lookup failed"),
],
None,
);
output.error_event(
command_name,
"failed",
&[
field("status", "failed"),
field("repo", &repo.repository_id.0),
field("error", "workspace root lookup failed"),
],
None,
)?;
return Err(reported_error(message));
}
};
let db_path = ensure_storage_db_path_for_write(root, command_name)?;
let storage = Storage::new(&db_path);
let repaired_categories = match initialize_storage_with_auto_repair(&storage) {
Ok(categories) => categories,
Err(err) => {
report_storage_failure(
output,
command_name,
repositories.len(),
&repo.repository_id.0,
root,
&db_path,
&err,
)?;
return Err(reported_error(format!(
"{command_name} failed for repository_id={} root={} db={}: {err}",
repo.repository_id.0,
root.display(),
db_path.display()
)));
}
};
if !repaired_categories.is_empty() {
output.progress_event(
OutputLevel::Warn,
"storage",
"auto_repair",
&[
field("status", "ok"),
field("command", command_name),
field("repo", &repo.repository_id.0),
field("repaired", repaired_categories.join(",")),
field("db", db_path.display()),
],
Some(&root.display().to_string()),
)?;
}
let precise_counters = run_cli_precise_generation(
&precise_server,
*output,
command_name,
&repo.repository_id.0,
root,
&[],
&[],
);
total_precise.add(precise_counters);
let mut repo_fields = vec![
field("status", "ok"),
field("repo", &repo.repository_id.0),
field("db", db_path.display()),
];
repo_fields.extend(precise_counter_fields(precise_counters));
output.progress_event(
OutputLevel::Ok,
command_name,
"repo",
&repo_fields,
Some(&root.display().to_string()),
)?;
}
let mut summary_fields = vec![field("status", "ok"), field("repos", repositories.len())];
summary_fields.extend(precise_counter_fields(total_precise));
output.summary_event(
OutputLevel::Ok,
command_name,
"complete",
&summary_fields,
None,
)?;
Ok(())
}
#[cfg(test)]
pub(crate) fn run_storage_maintenance_command(
config: &FriggConfig,
command: StorageMaintenanceCommand,
) -> Result<(), Box<dyn Error>> {
run_storage_maintenance_command_with_output(config, command, &CliOutput::normal())
}
pub(crate) fn run_storage_maintenance_command_with_output(
config: &FriggConfig,
command: StorageMaintenanceCommand,
output: &CliOutput,
) -> Result<(), Box<dyn Error>> {
let repositories = config.repositories();
let command_name = match command {
StorageMaintenanceCommand::RepairSemanticVectorStore => "repair-storage",
StorageMaintenanceCommand::Prune { .. } => "prune-storage",
};
let mut total_repaired = 0usize;
let mut total_manifest_snapshots_deleted = 0usize;
let mut start_fields = vec![
field("status", "starting"),
field("repos", repositories.len()),
];
if let StorageMaintenanceCommand::Prune {
keep_manifest_snapshots,
} = command
{
start_fields.push(field("keep_manifest_snapshots", keep_manifest_snapshots));
}
output.progress_event(
OutputLevel::Info,
command_name,
"start",
&start_fields,
None,
)?;
for repo in &repositories {
let root = match config.root_by_repository_id(&repo.repository_id.0) {
Some(root) => root,
None => {
let message = format_output_event_line(
OutputLevel::Error,
command_name,
"failed",
&[
field("status", "failed"),
field("repo", &repo.repository_id.0),
field("error", "workspace root lookup failed"),
],
None,
);
output.error_event(
command_name,
"failed",
&[
field("status", "failed"),
field("repo", &repo.repository_id.0),
field("error", "workspace root lookup failed"),
],
None,
)?;
return Err(reported_error(message));
}
};
let db_path = resolve_storage_db_path(root, command_name)?;
let storage = Storage::new(&db_path);
if let Err(err) = storage.require_current_schema() {
report_storage_failure(
output,
command_name,
repositories.len(),
&repo.repository_id.0,
root,
&db_path,
&err,
)?;
return Err(reported_error(format!(
"{command_name} failed for repository_id={} root={} db={}: {err}",
repo.repository_id.0,
root.display(),
db_path.display()
)));
}
match command {
StorageMaintenanceCommand::RepairSemanticVectorStore => {
if let Err(err) = storage.repair_semantic_vector_store() {
report_storage_failure(
output,
command_name,
repositories.len(),
&repo.repository_id.0,
root,
&db_path,
&err,
)?;
return Err(reported_error(format!(
"{command_name} failed for repository_id={} root={} db={}: {err}",
repo.repository_id.0,
root.display(),
db_path.display()
)));
}
if let Err(err) = storage.verify_runtime_readiness() {
report_storage_failure(
output,
command_name,
repositories.len(),
&repo.repository_id.0,
root,
&db_path,
&err,
)?;
return Err(reported_error(format!(
"{command_name} failed for repository_id={} root={} db={}: {err}",
repo.repository_id.0,
root.display(),
db_path.display()
)));
}
total_repaired += 1;
output.progress_event(
OutputLevel::Ok,
command_name,
"repo",
&[
field("status", "ok"),
field("repo", &repo.repository_id.0),
field("repaired", "semantic_vector_store"),
field("db", db_path.display()),
],
Some(&root.display().to_string()),
)?;
}
StorageMaintenanceCommand::Prune {
keep_manifest_snapshots,
} => {
let deleted_manifest_snapshots = match storage
.prune_repository_snapshots(&repo.repository_id.0, keep_manifest_snapshots)
{
Ok(deleted) => deleted,
Err(err) => {
report_storage_failure(
output,
command_name,
repositories.len(),
&repo.repository_id.0,
root,
&db_path,
&err,
)?;
return Err(reported_error(format!(
"{command_name} failed for repository_id={} root={} db={}: {err}",
repo.repository_id.0,
root.display(),
db_path.display()
)));
}
};
if let Err(err) = storage.verify_relational_schema() {
report_storage_failure(
output,
command_name,
repositories.len(),
&repo.repository_id.0,
root,
&db_path,
&err,
)?;
return Err(reported_error(format!(
"{command_name} failed for repository_id={} root={} db={}: {err}",
repo.repository_id.0,
root.display(),
db_path.display()
)));
}
total_manifest_snapshots_deleted += deleted_manifest_snapshots;
output.progress_event(
OutputLevel::Ok,
command_name,
"repo",
&[
field("status", "ok"),
field("repo", &repo.repository_id.0),
field("keep_manifest_snapshots", keep_manifest_snapshots),
field("manifest_snapshots_deleted", deleted_manifest_snapshots),
field("db", db_path.display()),
],
Some(&root.display().to_string()),
)?;
}
}
}
match command {
StorageMaintenanceCommand::RepairSemanticVectorStore => {
output.summary_event(
OutputLevel::Ok,
command_name,
"complete",
&[
field("status", "ok"),
field("repos", repositories.len()),
field("repaired", total_repaired),
],
None,
)?;
}
StorageMaintenanceCommand::Prune {
keep_manifest_snapshots,
} => {
output.summary_event(
OutputLevel::Ok,
command_name,
"complete",
&[
field("status", "ok"),
field("repos", repositories.len()),
field("keep_manifest_snapshots", keep_manifest_snapshots),
field(
"manifest_snapshots_deleted",
total_manifest_snapshots_deleted,
),
],
None,
)?;
}
}
Ok(())
}