Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ All notable Darc release changes should be summarized here.

## Unreleased

- Add `darc index --rebuild` to recreate the shared SQLite index from every configured project's archived sessions, and point users to it when the local index cannot be opened or migrated.
- Show GitHub Release titles as tags, such as `v0.1.6`, while preserving dated changelog headings.
- Narrow internal Rust storage APIs so SQLite schema details are no longer exposed outside the storage crate.
- Streamline public documentation around JSON query contracts and remove the internal backlog from docs.
Expand Down
13 changes: 10 additions & 3 deletions crates/cli/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ pub(crate) enum Commands {
)]
Sync(SyncArgs),
#[command(
about = "Index archived sessions for the active project into SQLite",
long_about = "Index archived sessions for the active project into the shared Darc SQLite database.\n\nRun this after `darc sync` when you want to rebuild searchable/queryable state without copying new archive files.",
about = "Index archived sessions into SQLite",
long_about = "Index archived sessions into the shared Darc SQLite database.\n\nWithout `--rebuild`, this indexes the active project. Run it after `darc sync` when you want to refresh searchable/queryable state without copying new archive files.\n\nUse `--rebuild` only when Darc reports that the SQLite index cannot be opened or migrated. Rebuild deletes the shared local index cache, then recreates it from every configured project's archived sessions.",
after_help = index_after_help()
)]
Index(IndexArgs),
Expand Down Expand Up @@ -474,10 +474,17 @@ pub(crate) struct IndexArgs {
long = "provider",
value_enum,
help_heading = "Selection",
help = "Limit indexing to the selected providers"
help = "Limit non-rebuild indexing to the selected providers"
)]
pub(crate) provider: Vec<ProviderArg>,

#[arg(
long,
help_heading = "Mode",
help = "Delete the shared SQLite index and rebuild it from every configured project's archived sessions"
)]
pub(crate) rebuild: bool,

#[arg(
long,
default_value_os_t = default_root_path(),
Expand Down
5 changes: 4 additions & 1 deletion crates/cli/src/args/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ pub(crate) fn sync_after_help() -> String {

/// Returns index command examples.
pub(crate) fn index_after_help() -> String {
styled_help_section("Examples", " darc index\n darc index --provider claude")
styled_help_section(
"Examples",
" darc index\n darc index --provider claude\n darc index --rebuild",
)
}

/// Returns canonical search command examples.
Expand Down
7 changes: 5 additions & 2 deletions crates/cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#[cfg(target_os = "windows")]
compile_error!("Darc CLI does not support Windows.");

mod agent_help;
mod args;
mod output;
Expand Down Expand Up @@ -26,7 +29,7 @@ use clap::{FromArgMatches, error::ErrorKind};
use darc_core::query::SearchEvidenceField;
#[cfg(test)]
use output::*;
use output::{format_json_clap_error, format_query_error};
use output::{HumanStyle, format_json_clap_error, format_query_error, format_standard_error};
use project::{run_init, run_link, run_project, run_remove, run_rename_from};
#[cfg(test)]
use query_commands::*;
Expand Down Expand Up @@ -133,7 +136,7 @@ fn standard_exit(result: Result<()>) -> i32 {
match result {
Ok(()) => 0,
Err(error) => {
eprintln!("error: {error:#}");
eprintln!("{}", format_standard_error(&error, HumanStyle::stderr()));
1
}
}
Expand Down
75 changes: 74 additions & 1 deletion crates/cli/src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use anyhow::{Context, Result};
use darc_core::query::{
QueryProtocolError, SearchMode, SearchSnippetMatcher, SearchTurnsQueryData, TurnsView,
};
use darc_core::{IndexDatabaseRebuildRecommendation, index_rebuild_command};
use darc_paths::current_utc_timestamp;
use serde::Serialize;
use serde_json::{Value as JsonValue, json};
Expand Down Expand Up @@ -533,7 +534,7 @@ pub(crate) fn format_query_error(error: &anyhow::Error) -> String {
generated_at: current_utc_timestamp(),
darc_version: env!("CARGO_PKG_VERSION"),
error: QueryErrorData {
message: error.to_string(),
message: format_query_error_message(error),
code: structured
.map(QueryProtocolError::code)
.or_else(|| read_validation.map(|error| error.code))
Expand All @@ -550,6 +551,78 @@ pub(crate) fn format_query_error(error: &anyhow::Error) -> String {
})
}

/// Returns one query error message with actionable index rebuild guidance when available.
fn format_query_error_message(error: &anyhow::Error) -> String {
if let Some(rebuild) = error
.chain()
.find_map(|cause| cause.downcast_ref::<IndexDatabaseRebuildRecommendation>())
{
return format!(
"local SQLite index at {} needs to be rebuilt; run `{}` to rebuild the shared index cache from archived sessions",
rebuild.path().display(),
index_rebuild_command(rebuild.path())
);
}

error.to_string()
}

/// Formats one standard human-facing command error.
pub(crate) fn format_standard_error(error: &anyhow::Error, style: HumanStyle) -> String {
if let Some(rebuild) = error
.chain()
.find_map(|cause| cause.downcast_ref::<IndexDatabaseRebuildRecommendation>())
{
return format_index_rebuild_error(error, rebuild, style);
}

format!("error: {error:#}")
}

/// Formats the actionable recovery message for an unusable SQLite index.
fn format_index_rebuild_error(
error: &anyhow::Error,
rebuild: &IndexDatabaseRebuildRecommendation,
style: HumanStyle,
) -> String {
let mut lines = vec![
"error: local SQLite index needs to be rebuilt".to_owned(),
format!(" Index DB: {}", style.path(rebuild.path().display())),
format!(
" Run: {}",
style.bold(index_rebuild_command(rebuild.path()))
),
" This deletes only the shared SQLite index cache, then rebuilds it from every configured project's archived sessions.".to_owned(),
" Archived sessions are not deleted.".to_owned(),
];
if let Some(cause) = index_rebuild_source_detail(error) {
lines.push(format!(" Cause: {cause}"));
}
lines.join("\n")
}

/// Returns source details after the rebuild recommendation in an error chain.
fn index_rebuild_source_detail(error: &anyhow::Error) -> Option<String> {
let mut saw_rebuild = false;
let mut details = Vec::new();
for cause in error.chain() {
if saw_rebuild {
details.push(cause.to_string());
} else if cause
.downcast_ref::<IndexDatabaseRebuildRecommendation>()
.is_some()
{
saw_rebuild = true;
}
}

if details.is_empty() {
None
} else {
Some(details.join(": "))
}
}

/// Returns one machine-readable JSON error envelope string for JSON parse failures.
pub(crate) fn format_json_clap_error(error: &clap::Error, args: &[OsString]) -> String {
let message = normalize_json_clap_error_message(error.to_string().trim_end().to_owned(), args);
Expand Down
144 changes: 133 additions & 11 deletions crates/cli/src/sync_index.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
use std::path::Path;
use std::{
env,
io::{self, IsTerminal},
path::{Path, PathBuf},
};

use anyhow::Result;
use anyhow::{Result, bail};
use darc_core::{
IndexOptions, IndexReport, SkippedRollout, SourceKind, SyncOptions, SyncReport, execute_sync,
index_project_sessions, prepare_sync,
IndexOptions, IndexReport, SkippedRollout, SourceKind, SyncOptions, SyncReport,
WorkspaceIndexReport, execute_sync, index_project_sessions, prepare_sync,
rebuild_workspace_index,
};

use crate::args::{IndexArgs, ProviderArg, SyncArgs};
use crate::output::{HumanStyle, print_field, print_line, print_section, print_warning};
use crate::output::{
HumanStyle, print_field, print_line, print_project_warning, print_section, print_warning,
};

/// Prepares and optionally executes the project-scoped sync workflow.
pub(crate) fn run_sync(args: SyncArgs) -> Result<()> {
Expand Down Expand Up @@ -183,12 +190,25 @@ pub(crate) fn add_init_hint_for_unconfigured_project(error: anyhow::Error) -> an

/// Indexes archived sessions for the active project into SQLite.
pub(crate) fn run_index(args: IndexArgs) -> Result<()> {
let report = index_project_sessions(
Some(args.root),
IndexOptions {
provider_filter: args.provider.into_iter().map(ProviderArg::into).collect(),
},
)?;
let IndexArgs {
provider,
rebuild,
root,
} = args;
if rebuild && !provider.is_empty() {
bail!(
"`darc index --rebuild` rebuilds all providers; remove `--provider` and run it again"
);
}

let _lock = acquire_index_lock_if_configured(&root)?;
if rebuild {
return run_index_rebuild(root);
}
let options = IndexOptions {
provider_filter: provider.into_iter().map(ProviderArg::into).collect(),
};
let report = index_project_sessions(Some(root), options)?;
let style = HumanStyle::stdout();

for skipped in &report.skipped_rollouts {
Expand Down Expand Up @@ -216,6 +236,94 @@ pub(crate) fn run_index(args: IndexArgs) -> Result<()> {
Ok(())
}

/// Acquires the shared index writer lock when this Darc root is configured.
fn acquire_index_lock_if_configured(root: &Path) -> Result<Option<crate::refresh::RefreshLock>> {
if root.join("config.toml").exists() {
crate::refresh::acquire_refresh_lock(root).map(Some)
} else {
Ok(None)
}
}

/// Rebuilds the shared workspace index from every configured project.
fn run_index_rebuild(root: PathBuf) -> Result<()> {
print_index_rebuild_started();
let report = rebuild_workspace_index(Some(root))?;
let style = HumanStyle::stdout();

for project in &report.projects {
for skipped in &project.skipped_rollouts {
print_project_warning(&project.project_name, format_skipped_rollout(skipped));
}
}

print_workspace_rebuild_summary(style, &report);
println!();
print_section(style, "Status");
let status = if report.skipped_rollout_count() == 0 {
style.ok("indexed")
} else {
style.warn("indexed with skipped rollouts")
};
print_field(style, 2, "Overall", status);

Ok(())
}

/// Prints one short rebuild progress line when stderr is interactive.
fn print_index_rebuild_started() {
let term = env::var("TERM").ok();
if io::stderr().is_terminal() && term.as_deref() != Some("dumb") {
eprintln!("{}", format_index_rebuild_started(HumanStyle::stderr()));
}
}

/// Formats the initial rebuild progress line.
fn format_index_rebuild_started(style: HumanStyle) -> String {
format!(
"Rebuilding {} from archived sessions...",
style.bold("shared index")
)
}

/// Prints one workspace-wide rebuild summary.
fn print_workspace_rebuild_summary(style: HumanStyle, report: &WorkspaceIndexReport) {
print_section(style, "Index Rebuild");
print_field(style, 2, "Root", style.path(report.root.display()));
print_field(style, 2, "Providers", format_sources(&report.providers));
print_field(
style,
2,
"Index DB",
style.path(report.index_db_path.display()),
);
print_field(
style,
2,
"Projects indexed",
style.count(report.projects.len()),
);
print_field(
style,
2,
"Sessions currently indexed",
style.count(report.sessions_currently_indexed()),
);
print_field(
style,
2,
"Turns currently indexed",
style.count(report.turns_currently_indexed()),
);
let skipped = report.skipped_rollout_count();
let skipped = if skipped == 0 {
style.ok(skipped)
} else {
style.warn(skipped)
};
print_field(style, 2, "Skipped rollout files", skipped);
}

/// Formats a source list for compact CLI output.
pub(crate) fn format_sources(sources: &[SourceKind]) -> String {
sources
Expand Down Expand Up @@ -251,3 +359,17 @@ pub(crate) fn format_skipped_rollout(skipped: &SkippedRollout) -> String {
)
}
}

#[cfg(test)]
mod tests {
use super::{HumanStyle, format_index_rebuild_started};

#[test]
fn index_rebuild_started_message_is_short_and_styled() {
let plain = format_index_rebuild_started(HumanStyle { enabled: false });
assert_eq!(plain, "Rebuilding shared index from archived sessions...");

let styled = format_index_rebuild_started(HumanStyle { enabled: true });
assert!(styled.contains("\x1b[1mshared index\x1b[0m"));
}
}
1 change: 1 addition & 0 deletions crates/cli/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use darc_core::{
IndexReport, RefreshAllBestEffortReport, RefreshProgress, RefreshProjectAttempt,
RefreshProjectFailure, RefreshReport, SourceKind, SyncReport,
config::{ClaudeSourceConfig, CodexSourceConfig, SharedConfig, SourcesConfig, WatchConfig},
index_rebuild_command,
};
use darc_rollout_audit::claude::{
ClaudeSchemaAuditFailure, ClaudeSchemaAuditReport, ClaudeSchemaDrift,
Expand Down
Loading
Loading