use anyhow::Result;
use clap::Parser;
use std::io::IsTerminal;
use std::path::PathBuf;
use crate::query::Scope;
#[derive(Parser, Debug)]
#[command(after_long_help = WRAPPER_HELP)]
pub struct QueryArgs {
filter: String,
#[arg(long)]
source: Option<String>,
#[arg(long = "id")]
ids: Vec<String>,
#[arg(long)]
input: Vec<String>,
#[arg(long)]
project: Option<PathBuf>,
#[arg(long)]
kind: Option<String>,
#[arg(short = 'c', long)]
compact: bool,
#[arg(short = 'r', long)]
raw: bool,
#[arg(long)]
no_sync: bool,
}
const WRAPPER_HELP: &str = "\
Each array element wraps a Toolpath step:
{
\"cache_id\": \"claude-abc123\",
\"path\": { \"id\": …, \"base\": …, \"meta\": { \"kind\": …, \"source\": … } },
\"step\": { \"id\": …, \"parents\": [...], \"actor\": …, \"timestamp\": … },
\"change\": { \"<artifact>\": { \"raw\": …, \"structural\": … } },
\"meta\": { … },
\"dead_end\": false
}
The fields under `change[].structural` are defined by the path's kind;
run `path kind <kind>` for the schema, or `path query --kind … '.[0]'` to
read a sample. Identity is the triple (cache_id, path.id, step.id).
A real cache mixes provider and tool versions, so a field's shape can vary
(e.g. `tool_uses[]` is sometimes an object, sometimes a bare name string).
Guard element access with `?` (`.name?`) and `// empty` so one odd value
doesn't abort the run; sample with `'.[0]'` when unsure.
Examples:
path query 'map(select(any(.. | strings; test(\"RefCell\"))))'
path query 'map(select(any(.change | keys[]; endswith(\"cmd_resume.rs\"))))'
path query --source claude 'map(select(any(.change[].structural.tool_uses[]?; .name? == \"Bash\" and .result.is_error? == true)))'
path query 'group_by(.path.meta.source) | map({source: .[0].path.meta.source, steps: length})'
path query -r '.[].cache_id' | sort -u # raw ids, pipeable to xargs/grep
path query -r '.[0].change[].structural.text' # read a turn's text, unescaped";
pub fn run(args: QueryArgs, pretty: bool) -> Result<()> {
#[cfg(not(target_os = "emscripten"))]
if !args.no_sync {
sync_query_scope(&args);
}
let scope = Scope {
source: args.source,
ids: args.ids,
inputs: args.input,
project: args.project,
kind: args.kind,
};
let compact = args.compact || (!pretty && !std::io::stdout().is_terminal());
crate::query::run(&scope, &args.filter, compact, args.raw)
}
#[cfg(not(target_os = "emscripten"))]
fn sync_query_scope(args: &QueryArgs) {
let types = sync_types_for(args.source.as_deref(), &args.ids, &args.input);
if types.is_empty() {
return;
}
let bundle = crate::harness::HarnessBundle::from_environment();
match crate::sync::sync_bundle(&bundle, &types, &mut ()) {
Ok(outcomes) => {
for (t, o) in outcomes {
if o.new + o.updated + o.failed > 0 {
let failed = if o.failed > 0 {
format!(", {} failed", o.failed)
} else {
String::new()
};
eprintln!(
"synced {}: {} new, {} updated{failed}",
t.name(),
o.new,
o.updated
);
}
}
}
Err(e) => eprintln!("warning: cache sync skipped: {e}"),
}
}
#[cfg(not(target_os = "emscripten"))]
fn sync_types_for(
source: Option<&str>,
ids: &[String],
inputs: &[String],
) -> Vec<crate::artifact::ArtifactType> {
use crate::artifact::ArtifactType;
let scans_cache = source.is_some() || !ids.is_empty() || inputs.is_empty();
if !scans_cache {
return Vec::new();
}
if let Some(source) = source {
return ArtifactType::parse(source).into_iter().collect();
}
if !ids.is_empty() {
return ArtifactType::ALL
.into_iter()
.filter(|t| {
ids.iter().any(|id| {
id.strip_prefix(t.name())
.is_some_and(|rest| rest.starts_with('-'))
})
})
.collect();
}
ArtifactType::ALL.to_vec()
}
#[cfg(all(test, not(target_os = "emscripten")))]
mod tests {
use super::sync_types_for;
use crate::artifact::ArtifactType;
fn s(v: &[&str]) -> Vec<String> {
v.iter().map(|s| s.to_string()).collect()
}
#[test]
fn input_only_queries_sync_nothing() {
assert!(sync_types_for(None, &[], &s(&["doc.json"])).is_empty());
}
#[test]
fn source_flag_narrows_to_one_type() {
assert_eq!(
sync_types_for(Some("claude"), &[], &[]),
vec![ArtifactType::Claude]
);
assert!(
sync_types_for(Some("pathbase"), &[], &[]).is_empty(),
"non-syncable sources sync nothing"
);
}
#[test]
fn ids_narrow_to_their_prefixes() {
let types = sync_types_for(
None,
&s(&["claude-abc", "codex-def", "pathbase-x-y-z"]),
&[],
);
assert_eq!(types, vec![ArtifactType::Claude, ArtifactType::Codex]);
assert_eq!(
sync_types_for(None, &s(&["cursor-abc"]), &[]),
vec![ArtifactType::Cursor]
);
}
#[test]
fn bare_cache_query_syncs_everything() {
assert_eq!(sync_types_for(None, &[], &[]), ArtifactType::ALL.to_vec());
}
#[test]
fn source_beats_ids_and_inputs_do_not_disable_cache_scan() {
assert_eq!(
sync_types_for(Some("git"), &s(&["claude-abc"]), &s(&["doc.json"])),
vec![ArtifactType::Git]
);
}
}