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
71 changes: 54 additions & 17 deletions crates/autophagy-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -718,7 +718,7 @@ impl ChallengeCheck {
enum CommandReport {
Import(ImportReport),
Sessions(Vec<SessionSummary>),
Search(Vec<RetrievalHit>),
Search(SearchReport),
Digest(DigestReport),
Patterns(PatternsReport),
#[serde(rename = "mutations_propose")]
Expand Down Expand Up @@ -753,6 +753,25 @@ enum CommandReport {
Config(config::ConfigReport),
}

/// Retrieval hits paired with whether the database held any events at all.
///
/// The flag lets the text renderer distinguish "nothing was ever imported" from
/// "imported, but this query matched nothing" — two very different situations
/// for a new user. It never reaches the JSON surface: [`Serialize`] emits only
/// the bare hit array, so `--output json` stays an array and machine consumers
/// see no prose (an empty result is still `[]`).
#[derive(Debug)]
struct SearchReport {
hits: Vec<RetrievalHit>,
database_empty: bool,
}

impl Serialize for SearchReport {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.hits.serialize(serializer)
}
}

#[derive(Debug, Serialize)]
struct ReindexReport {
index_tool_input: bool,
Expand Down Expand Up @@ -1278,7 +1297,14 @@ fn execute(
outcome: outcome.map(RetrievalOutcome::from),
limit,
};
Ok(CommandReport::Search(store.retrieve(&retrieval)?))
let hits = store.retrieve(&retrieval)?;
// A zero-event store means nothing was ever imported, which the text
// renderer surfaces differently from a query that simply missed.
let database_empty = store.stats()?.events == 0;
Ok(CommandReport::Search(SearchReport {
hits,
database_empty,
}))
}
Commands::Digest {
project,
Expand Down Expand Up @@ -2025,24 +2051,35 @@ fn write_report(
}
},
CommandReport::Sessions(sessions) => {
writeln!(writer, "SESSION\tSOURCE\tEVENTS\tLAST EVENT\tPROJECT")?;
for session in sessions {
writeln!(
writer,
"{}\t{}\t{}\t{}\t{}",
session.session_id,
session.adapter,
session.event_count,
session.last_event_at,
session.project_path.as_deref().unwrap_or("-")
)?;
if sessions.is_empty() {
writeln!(writer, "no sessions imported yet — run `autophagy setup`")?;
} else {
writeln!(writer, "SESSION\tSOURCE\tEVENTS\tLAST EVENT\tPROJECT")?;
for session in sessions {
writeln!(
writer,
"{}\t{}\t{}\t{}\t{}",
session.session_id,
session.adapter,
session.event_count,
session.last_event_at,
session.project_path.as_deref().unwrap_or("-")
)?;
}
}
}
CommandReport::Search(hits) => {
if hits.is_empty() {
writeln!(writer, "no retrieval matches")?;
CommandReport::Search(report) => {
if report.hits.is_empty() {
if report.database_empty {
writeln!(
writer,
"no events imported yet — run `autophagy setup` to import your sessions"
)?;
} else {
writeln!(writer, "no retrieval matches")?;
}
}
for hit in hits {
for hit in &report.hits {
writeln!(
writer,
"{}\t{}\t{} bps\t{}",
Expand Down
31 changes: 30 additions & 1 deletion crates/autophagy-cli/src/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,11 @@ pub fn run(
verbose,
};

// Whether the user pointed the database somewhere explicit. Setup's config
// file, by contrast, never moves with `--database`, so an explicit database
// is the signal that the two locations have diverged (see the global-config
// notice below).
let database_explicit = database.is_some();
let database = resolve_database_path(database)?;
let mut store = open_store(&database)?;

Expand All @@ -202,7 +207,10 @@ pub fn run(
let prior_interval = config.watch_interval_seconds;

ui.say("Autophagy setup — local, offline, nothing leaves your machine.");
if config.present {
// Only claim this is a re-run when a config file is actually on disk; a
// first run against an empty (or freshly relocated) config directory has no
// prior settings to show as defaults.
if crate::config::config_path()?.exists() {
ui.say("Re-running setup — current settings are shown as defaults.");
}
ui.say("");
Expand Down Expand Up @@ -428,6 +436,27 @@ pub fn run(
}

// 7. Persist the chosen settings so later runs and commands inherit them.
// Settings live in a single global config file that does NOT move with
// `--database`. When the user redirected the database to an explicit path
// but the config still lands in the shared application-support directory,
// say so plainly before writing — otherwise a throwaway `--database /tmp/…`
// run silently rewrites their real global config. AUTOPHAGY_CONFIG_DIR is
// the escape hatch that keeps a run fully isolated; surface it here.
let config_is_global = std::env::var_os(crate::config::CONFIG_DIR_ENV).is_none();
if config_is_global && database_explicit {
let notice = format!(
"note: settings are saved globally to {} (set {} to keep this run isolated)",
crate::config::config_path()?.display(),
crate::config::CONFIG_DIR_ENV,
);
if interactive {
ui.say(&notice);
} else {
// Non-interactive (`--yes`, including JSON): stdout carries the
// report, so the advisory goes to stderr where it can't corrupt it.
eprintln!("{notice}");
}
}
let selected_names = adapter_names(&selected);
let config_path = crate::config::write_setup(&crate::config::SetupValues {
adapters: selected_names.clone(),
Expand Down
37 changes: 34 additions & 3 deletions crates/autophagy-cli/src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
//! is a COUNT-style query plus one deterministic detection pass; it works
//! against an empty database and with no config file.

use std::{collections::BTreeMap, io::Write, path::PathBuf};
use std::{
collections::BTreeMap,
io::Write,
path::{Path, PathBuf},
};

use serde::Serialize;
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
Expand Down Expand Up @@ -122,7 +126,7 @@ pub fn run(
let existed_before = db_path.exists();

let store = open_store(&db_path)?;
let size_bytes = std::fs::metadata(&db_path).ok().map(|meta| meta.len());
let size_bytes = on_disk_size(&db_path);
let stats = store.stats()?;
let signatures = store.signature_count()?;
// Every indexed signature is an `operation/<version>|…` key. Rows that do not
Expand Down Expand Up @@ -250,7 +254,7 @@ pub fn write_text(report: &StatusReport, writer: &mut impl Write) -> std::io::Re
let freshness = if db.exists {
String::new()
} else {
" · new database (nothing imported yet)".to_owned()
" · created new database (nothing imported yet)".to_owned()
};
writeln!(
writer,
Expand Down Expand Up @@ -279,7 +283,13 @@ pub fn write_text(report: &StatusReport, writer: &mut impl Write) -> std::io::Re
"{} command signatures indexed · exact recall on",
group(i64::try_from(report.index.signatures).unwrap_or(i64::MAX))
)
} else if report.database.events == 0 {
// Nothing imported yet: reindex has nothing to rebuild, so point the new
// user at the first step that actually populates the database.
"commands not indexed — run `autophagy setup` to import your sessions".to_owned()
} else {
// Events exist but were imported without indexing; reindex is the right
// and only way to make their commands searchable.
"commands not indexed — run `autophagy reindex --index-tool-input`".to_owned()
};
writeln!(writer, "{}", row("Search", &search))?;
Expand Down Expand Up @@ -442,6 +452,27 @@ fn group(value: i64) -> String {
}
}

/// True on-disk footprint of the database: the main file plus its WAL and
/// shared-memory sidecars.
///
/// The store opens in WAL journal mode, so the pages written by the migrations
/// that `open_store` just ran live in `<db>-wal` until a checkpoint. Reading
/// only the main file's length right after opening therefore undercounts a
/// freshly created database — it reports the ~4 KiB header rather than the
/// migrated schema — which is exactly the number a new user sees and disbelieves.
/// Summing the sidecars makes the reported size match what the database occupies.
fn on_disk_size(db_path: &Path) -> Option<u64> {
let mut total = std::fs::metadata(db_path).ok()?.len();
for suffix in ["-wal", "-shm"] {
let mut sidecar = db_path.as_os_str().to_owned();
sidecar.push(suffix);
if let Ok(meta) = std::fs::metadata(PathBuf::from(sidecar)) {
total += meta.len();
}
}
Some(total)
}

fn format_bytes(bytes: u64) -> String {
const UNITS: [&str; 4] = ["B", "KiB", "MiB", "GiB"];
#[allow(clippy::cast_precision_loss)]
Expand Down
Loading
Loading