pub const SCHEMA_LIST_REQUEST: &str = "";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PrintSchemaPlan {
Defer,
Print(String),
List,
}
#[must_use]
pub fn scan_print_schema(argv: &[String]) -> Option<(String, usize)> {
const FLAG: &str = "--print-schema";
const FLAG_PREFIX: &str = "--print-schema=";
for (index, arg) in argv.iter().enumerate() {
if let Some(value) = arg.strip_prefix(FLAG_PREFIX) {
return Some((value.to_string(), index));
}
if arg == FLAG {
let separate = argv
.get(index + 1)
.map(String::as_str)
.filter(|s| !s.starts_with('-'));
return Some((separate.unwrap_or(SCHEMA_LIST_REQUEST).to_string(), index));
}
}
None
}
#[must_use]
pub fn plan_print_schema(argv: &[String]) -> PrintSchemaPlan {
let Some((name, token_index)) = scan_print_schema(argv) else {
return PrintSchemaPlan::Defer;
};
let command_index = argv
.iter()
.enumerate()
.skip(1)
.find(|(_, a)| !a.starts_with('-') && SUBCOMMANDS.contains(&a.as_str()))
.map(|(index, _)| index);
let flag_after_command = command_index.is_some_and(|command| token_index > command);
if flag_after_command && name.is_empty() {
return PrintSchemaPlan::Defer;
}
if name.is_empty() {
PrintSchemaPlan::List
} else {
PrintSchemaPlan::Print(name)
}
}
pub const SUBCOMMANDS: &[&str] = &[
"read",
"write",
"edit",
"search",
"replace",
"hash",
"delete",
"count",
"diff",
"move",
"copy",
"list",
"extract",
"calc",
"regex",
"transform",
"scope",
"batch",
"backup",
"rollback",
"apply",
"completions",
"prune-backups",
"edit-loop",
"get",
"set",
"del",
"outline",
"query",
"case",
"semantic-merge",
"sparse",
"recipe",
"stat",
"agent-surface",
"watch",
"codemod",
"semantic-search",
"verify",
"wal-stats",
"wal-heal",
"doctor",
"locale",
"config",
"commands",
"progress",
"error",
"best-candidate",
"cancelled",
];
pub(crate) fn command_schema_name(command: &Commands) -> &'static str {
match command {
Commands::Read(_) => "read",
Commands::Write(_) => "write",
Commands::Edit(_) => "edit",
Commands::Search(_) => "search",
Commands::Replace(_) => "replace",
Commands::Hash(_) => "hash",
Commands::Delete(_) => "delete",
Commands::Count(_) => "count",
Commands::Diff(_) => "diff",
Commands::Move(_) => "move",
Commands::Copy(_) => "copy",
Commands::List(_) => "list",
Commands::Extract(_) => "extract",
Commands::Calc(_) => "calc",
Commands::Regex(_) => "regex",
Commands::Transform(_) => "transform",
Commands::Batch(_) => "batch",
Commands::Scope(_) => "scope",
Commands::Backup(_) => "backup",
Commands::Rollback(_) => "rollback",
Commands::Apply(_) => "apply",
Commands::Set(_) => "set",
Commands::Get(_) => "get",
Commands::Del(_) => "del",
Commands::Case(_) => "case",
Commands::Query(_) => "query",
Commands::Outline(_) => "outline",
Commands::WalStats(_) => "wal-stats",
Commands::WalHeal(_) => "wal-heal",
Commands::PruneBackups(_) => "prune-backups",
Commands::EditLoop(_) => "edit-loop",
Commands::Verify(_) => "verify",
Commands::SemanticMerge(_) => "semantic-merge",
Commands::Sparse(_) => "sparse",
Commands::Recipe(_) => "recipe",
Commands::Stat(_) => "stat",
Commands::AgentSurface(_) => "agent-surface",
Commands::Watch(_) => "watch",
Commands::ExtractArtifact(_) => "extract-artifact",
Commands::Codemod(_) => "codemod",
Commands::SemanticSearch(_) => "semantic-search",
Commands::Scratch(_) => "scratch",
Commands::Doctor(_) => "doctor",
Commands::Locale(_) => "locale",
Commands::Config(_) => "config",
Commands::CommandsTree(_) => "commands",
Commands::Completions(_) => "completions",
}
}
fn emit_json_schema(command: &Commands, out: impl Write) -> Result<()> {
emit_schema_by_name(command_schema_name(command), out).map(|_| ())
}
fn writer_stamped_properties() -> Vec<(&'static str, serde_json::Value)> {
vec![
(
"ok",
serde_json::json!({"type": "boolean", "description": "Outcome of the run, stamped by the writer"}),
),
(
"code",
serde_json::json!({"type": "string", "description": "Machine error code, present when ok is false"}),
),
(
"exit",
serde_json::json!({"type": "integer", "minimum": 0, "description": "Process exit status, present when ok is false"}),
),
(
"target_resolved",
serde_json::json!({"type": "array", "items": {"type": "string"}, "description": "Absolute targets the run resolved, in invocation order, and never a joined string, so a path holding `,` stays readable; a run with one target publishes a one-item array, so the shape never depends on the count"}),
),
(
"target_source",
serde_json::json!({"type": "string", "enum": ["argv", "workspace", "stdin"], "description": "Where the target came from: named in the invocation line, inherited from the workspace slot, or the standard-input stream"}),
),
(
"workspace_source",
serde_json::json!({"type": "string", "enum": ["argv", "cwd"], "description": "Where the workspace root came from: the `--workspace` flag or the process working directory"}),
),
(
"records_suppressed",
serde_json::json!({"type": "integer", "minimum": 0, "description": "Records the writer dropped before the terminal event: `--filter` rejections, `--select` projections that emptied the record, dedupe duplicates and `--limit` cuts. It counts records, never files, so it does not compare with `files_matched`: one file can contribute several records"}),
),
(
"files_budget_dropped",
serde_json::json!({"type": "integer", "minimum": 0, "description": "Entries the `--limit` budget dropped, in the unit the verb produces: files for walking verbs, records for streaming ones; a floor, because workers may stop before producing the rest"}),
),
(
"files_planned",
serde_json::json!({"type": "integer", "minimum": 0, "description": "Files the run would have modified in `--dry-run` or `--preview`; absent from a run that really wrote"}),
),
(
"truncated",
serde_json::json!({"type": "boolean", "description": "The record itself was cut by `--truncate-content` or `--max-output-bytes`, so the fields are shorter than the source"}),
),
(
"truncated_records",
serde_json::json!({"type": "integer", "minimum": 0, "description": "Records the writer cut this run, counted once per record"}),
),
]
}
fn with_envelope_keys(mut schema: schemars::Schema) -> schemars::Schema {
if let Some(root) = schema.as_object_mut() {
let properties = root
.entry("properties")
.or_insert_with(|| serde_json::json!({}));
if let Some(map) = properties.as_object_mut() {
for (key, definition) in writer_stamped_properties() {
map.entry(key).or_insert(definition);
}
}
}
schema
}
fn error_output_schema() -> schemars::Schema {
let mut schema = with_envelope_keys(schemars::schema_for!(crate::error::ErrorJson));
if let Some(required) = schema
.as_object_mut()
.and_then(|root| root.get_mut("required"))
.and_then(serde_json::Value::as_array_mut)
{
required.retain(|key| {
!matches!(
key.as_str(),
Some("message" | "error_class" | "retryable")
)
});
}
schema
}
fn file_verb_schema(name: &str) -> Option<schemars::Schema> {
Some(match name {
"read" | "stat" => schemars::schema_for!(ndjson_types::ReadOutput),
"write" => schemars::schema_for!(ndjson_types::WriteOutput),
"write-risk" => schemars::schema_for!(ndjson_types::WriteRiskAssessment),
"edit" => schemars::schema_for!(ndjson_types::EditOutput),
"search" => return search_output_schema(),
"replace" => return replace_output_schema(),
"hash" => {
return record_with_summary_schema(
schemars::schema_for!(ndjson_types::HashOutput),
"HashOutput",
"Records of `hash`: one digest per file, then the terminal summary.",
);
}
"verify" => {
return record_with_summary_schema(
schemars::schema_for!(ndjson_types::HashVerifyOutput),
"HashVerifyOutput",
"Records of `verify`: one verdict per file, then the terminal summary.",
);
}
"delete" => {
return record_with_summary_schema(
schemars::schema_for!(ndjson_types::DeleteOutput),
"DeleteOutput",
"Records of `delete`: one deleted file, then the terminal summary.",
);
}
"count" => schemars::schema_for!(ndjson_types::CountTotalOutput),
"count-by-ext" => schemars::schema_for!(ndjson_types::CountByExtOutput),
"count-by-size" => schemars::schema_for!(ndjson_types::CountBySizeOutput),
"diff" => schemars::schema_for!(ndjson_types::DiffOutput),
"move" => schemars::schema_for!(ndjson_types::MoveOutput),
"copy" => {
return record_with_summary_schema(
schemars::schema_for!(ndjson_types::CopyOutput),
"CopyOutput",
"Records of `copy`: one copied file, then the terminal summary.",
);
}
"list" => return list_output_schema(),
"extract" => return extract_output_schema(),
"calc" => schemars::schema_for!(ndjson_types::CalcOutput),
"regex" => schemars::schema_for!(ndjson_types::RegexOutput),
"transform" => {
return record_with_summary_schema(
schemars::schema_for!(ndjson_types::TransformResult),
"TransformOutput",
"Records of `transform`: one transformed file, then the terminal summary.",
);
}
"batch" => return batch_output_schema(),
"scope" => return scope_output_schema(),
"backup" => return backup_output_schema(),
"rollback" => schemars::schema_for!(ndjson_types::RollbackResult),
"apply" => return apply_output_schema(),
"prune-backups" => {
return record_with_own_summary(
schemars::schema_for!(ndjson_types::PruneBackupEntry),
schemars::schema_for!(ndjson_types::PruneBackupSummary),
"PruneBackupsOutput",
"Records of `prune-backups`: one entry per backup handled, then the terminal summary.",
);
}
"edit-loop" => schemars::schema_for!(ndjson_types::EditLoopSummary),
"completions" => schemars::schema_for!(CompletionsInstalled),
"extract-artifact" => {
schemars::schema_for!(crate::commands::extract_artifact::ArtifactOutput)
}
"recipe" => return recipe_output_schema(),
"watch" => return watch_output_schema(),
_ => return None,
})
}
fn report_verb_schema(name: &str) -> Option<schemars::Schema> {
Some(match name {
"set" => schemars::schema_for!(crate::commands::set::SetResult),
"get" => schemars::schema_for!(crate::commands::get::GetResult),
"del" => schemars::schema_for!(crate::commands::del::DelResult),
"case" => return case_output_schema(),
#[cfg(feature = "ast")]
"query" => schemars::schema_for!(crate::commands::query::QuerySummary),
#[cfg(feature = "ast")]
"outline" => schemars::schema_for!(crate::commands::outline::OutlineSummary),
#[cfg(not(feature = "ast"))]
"query" | "outline" => schemars::schema_for!(ndjson_types::ProgressEvent),
"locale" => schemars::schema_for!(crate::commands::locale_cmd::LocaleReport),
"config" => schemars::schema_for!(crate::commands::config_cmd::ConfigOutput),
"scratch" => schemars::schema_for!(crate::commands::scratch::ScratchInitEvent),
"wal-stats" => schemars::schema_for!(ndjson_types::WalStats),
"wal-heal" => schemars::schema_for!(ndjson_types::AutoHealReport),
"semantic-merge" => return semantic_merge_output_schema(),
"sparse" => return sparse_output_schema(),
"agent-surface" => schemars::schema_for!(crate::commands::agent_surface::SurfaceManifest),
"codemod" => return codemod_output_schema(),
"semantic-search" => return semantic_search_output_schema(),
"doctor" => schemars::schema_for!(crate::commands::doctor::DoctorReport),
"commands" => schemars::schema_for!(crate::commands::command_tree::CommandsTreeReport),
_ => return None,
})
}
fn event_schema(name: &str) -> Option<schemars::Schema> {
Some(match name {
"progress" => schemars::schema_for!(ndjson_types::ProgressEvent),
"error" => error_output_schema(),
"best-candidate" => schemars::schema_for!(ndjson_types::BestCandidate),
"cancelled" => schemars::schema_for!(ndjson_types::CancelledEvent),
"stream_diff" => schemars::schema_for!(crate::output::StreamDiffOutput),
_ => return None,
})
}
pub fn emit_schema_by_name(name: &str, mut out: impl Write) -> Result<bool> {
if name == SCHEMA_LIST_REQUEST {
let mut names: Vec<&str> = SCHEMA_NAMES.to_vec();
names.sort_unstable();
for name in names {
serde_json::to_writer(&mut out, &name).map_err(std::io::Error::from)?;
out.write_all(b"\n")?;
}
out.flush()?;
return Ok(true);
}
let verb = file_verb_schema(name).or_else(|| report_verb_schema(name));
let closed = verb
.map(|schema| {
let union_root = schema
.as_object()
.is_some_and(|root| root.contains_key("anyOf") || root.contains_key("oneOf"));
if name == "scope" || union_root {
schema
} else {
with_envelope_keys(schema)
}
})
.map(|schema| close_terminal_schema(name, schema));
let Some(schema) = closed.or_else(|| event_schema(name)) else {
return Ok(false);
};
serde_json::to_writer_pretty(&mut out, &schema).map_err(std::io::Error::from)?;
out.write_all(b"\n")?;
out.flush()?;
Ok(true)
}