feat(cli): alias subcommand; remove --alias flag (RFC-011 D4) (#244)

Operator aliases move from the --alias flag on query/mutate to a dedicated 'omnigraph alias <name> [args]' subcommand, so an alias can never shadow or be shadowed by a built-in verb. Unknown name errors listing defined aliases. Removes the legacy alias machinery from query/mutate (net -156 lines); legacy omnigraph.yaml aliases lose their CLI entry point.
This commit is contained in:
Andrew Altshuler 2026-06-15 15:23:03 +03:00 committed by GitHub
parent 2ed05d2cb1
commit b395757e21
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 128 additions and 379 deletions

View file

@ -99,12 +99,10 @@ pub(crate) enum Command {
legacy_uri: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long, conflicts_with_all = ["query", "query_string"])]
alias: Option<String>,
#[arg(long, conflicts_with_all = ["alias", "query_string"])]
#[arg(long, conflicts_with = "query_string")]
query: Option<PathBuf>,
/// Inline GQ source — alternative to `--query <path>` and `--alias <name>`.
#[arg(short = 'e', long = "query-string", value_name = "GQ", conflicts_with_all = ["query", "alias"])]
/// Inline GQ source — alternative to `--query <path>`.
#[arg(short = 'e', long = "query-string", value_name = "GQ", conflicts_with = "query")]
query_string: Option<String>,
#[arg(long)]
name: Option<String>,
@ -118,8 +116,6 @@ pub(crate) enum Command {
format: Option<ReadOutputFormat>,
#[arg(long, conflicts_with = "format")]
json: bool,
#[arg()]
alias_args: Vec<String>,
},
/// Execute a graph mutation query against a branch.
///
@ -135,12 +131,10 @@ pub(crate) enum Command {
legacy_uri: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long, conflicts_with_all = ["query", "query_string"])]
alias: Option<String>,
#[arg(long, conflicts_with_all = ["alias", "query_string"])]
#[arg(long, conflicts_with = "query_string")]
query: Option<PathBuf>,
/// Inline GQ source — alternative to `--query <path>` and `--alias <name>`.
#[arg(short = 'e', long = "query-string", value_name = "GQ", conflicts_with_all = ["query", "alias"])]
/// Inline GQ source — alternative to `--query <path>`.
#[arg(short = 'e', long = "query-string", value_name = "GQ", conflicts_with = "query")]
query_string: Option<String>,
#[arg(long)]
name: Option<String>,
@ -150,8 +144,28 @@ pub(crate) enum Command {
branch: Option<String>,
#[arg(long)]
json: bool,
#[arg()]
alias_args: Vec<String>,
},
/// Invoke an operator alias (RFC-011 Decision 4).
///
/// An alias is a personal binding under `aliases:` in
/// ~/.omnigraph/config.yaml — name → (server, graph, stored-query name,
/// default params). `omnigraph alias <name> [args]` invokes the bound
/// stored query on its server. Living in its own namespace, an alias can
/// never shadow or be shadowed by a built-in verb. Replaces the removed
/// `--alias` flag on `query`/`mutate`.
Alias {
/// Alias name (a key under `aliases:` in ~/.omnigraph/config.yaml).
name: String,
/// Positional args bound to the alias's declared `args` params, in order.
args: Vec<String>,
#[arg(long)]
config: Option<PathBuf>,
#[command(flatten)]
params: ParamsArgs,
#[arg(long, conflicts_with = "json")]
format: Option<ReadOutputFormat>,
#[arg(long, conflicts_with = "format")]
json: bool,
},
/// Load data into a graph (local or remote)
Load {

View file

@ -729,44 +729,6 @@ pub(crate) fn parse_alias_value(value: &str) -> Value {
serde_json::from_str(value).unwrap_or_else(|_| Value::String(value.to_string()))
}
pub(crate) fn merged_params_json(
alias_name: Option<&str>,
alias_arg_names: &[String],
alias_arg_values: &[String],
explicit: Option<Value>,
) -> Result<Option<Value>> {
if alias_arg_values.len() > alias_arg_names.len() {
let alias = alias_name.unwrap_or("<alias>");
bail!(
"alias '{}' expects at most {} args but got {}",
alias,
alias_arg_names.len(),
alias_arg_values.len()
);
}
let mut merged = serde_json::Map::new();
for (arg_name, arg_value) in alias_arg_names.iter().zip(alias_arg_values.iter()) {
merged.insert(arg_name.clone(), parse_alias_value(arg_value));
}
match explicit {
Some(Value::Object(object)) => {
for (key, value) in object {
merged.insert(key, value);
}
}
Some(_) => bail!("params JSON must be an object"),
None => {}
}
if merged.is_empty() {
Ok(None)
} else {
Ok(Some(Value::Object(merged)))
}
}
/// The format cascade (RFC-007 §D3): `--json` > `--format` > alias format >
/// legacy `cli.output_format` (RFC-008 window) > operator `defaults.output`
/// > table.
@ -790,43 +752,6 @@ pub(crate) fn resolve_read_format(
.unwrap_or_default()
}
pub(crate) fn resolve_alias<'a>(
config: &'a OmnigraphConfig,
alias_name: Option<&'a str>,
expected: AliasCommand,
) -> Result<Option<(&'a str, &'a omnigraph_server::AliasConfig)>> {
let Some(alias_name) = alias_name else {
return Ok(None);
};
let alias = config.alias(alias_name)?;
if alias.command != expected {
bail!(
"alias '{}' is a {:?} alias, not a {:?} alias",
alias_name,
alias.command,
expected
);
}
Ok(Some((alias_name, alias)))
}
pub(crate) fn normalize_legacy_alias_uri(
uri: Option<String>,
target_available: bool,
alias_name: Option<&str>,
mut alias_args: Vec<String>,
) -> (Option<String>, Vec<String>) {
let Some(candidate) = uri else {
return (None, alias_args);
};
if alias_name.is_some() && target_available {
alias_args.insert(0, candidate);
return (None, alias_args);
}
(Some(candidate), alias_args)
}
pub(crate) fn read_target_from_cli(branch: Option<String>, snapshot: Option<String>) -> ReadTarget {

View file

@ -28,7 +28,7 @@ use omnigraph_api_types::{
};
use omnigraph_server::queries::{QueryRegistry, check, format_check_breakages};
use omnigraph_server::{
AliasCommand, OmnigraphConfig, PolicyAction, PolicyDecision, PolicyEngine, PolicyRequest,
OmnigraphConfig, PolicyAction, PolicyDecision, PolicyEngine, PolicyRequest,
PolicyTestConfig, ReadOutputFormat, graph_resource_id_for_selection, load_config,
};
use reqwest::Method;
@ -569,7 +569,6 @@ async fn main() -> Result<()> {
uri,
legacy_uri,
config,
alias,
query,
query_string,
name,
@ -578,182 +577,61 @@ async fn main() -> Result<()> {
snapshot,
format,
json,
alias_args,
} => {
if alias.is_none() && query.is_none() && query_string.is_none() {
bail!("exactly one of --query, --query-string, or --alias must be provided");
if query.is_none() && query_string.is_none() {
bail!("provide a query: --query <file> or -e '<inline gq>'");
}
let config = load_cli_config(config.as_ref())?;
// Operator aliases (RFC-007 PR 3): pure bindings to stored
// queries. A legacy file-alias with the same name wins during
// the RFC-008 window (with a warning); an alias name found
// only in the operator layer takes the invoke path here.
if let Some(alias_name) = alias.as_deref() {
let operator_config = crate::operator::load_operator_config()?;
if let Some(operator_alias) = operator_config.aliases.get(alias_name) {
if config.alias(alias_name).is_ok() {
eprintln!(
"warning: alias '{alias_name}' is defined in both omnigraph.yaml (legacy, wins during the deprecation window) and the operator config; the legacy definition applies"
);
} else {
// The hidden legacy-uri positional swallows the first
// bare arg; an operator alias always knows its target,
// so reclaim it as the first positional param.
let (_, alias_args) = normalize_legacy_alias_uri(
legacy_uri.clone(),
true,
Some(alias_name),
alias_args.clone(),
);
let output = execute_operator_alias(
&http_client,
&config,
alias_name,
operator_alias,
&alias_args,
load_params_json(&params)?,
)
.await?;
let format =
resolve_read_format(&config, format, json, operator_alias.format);
print_read_output(&output, format, &config)?;
return Ok(());
}
}
}
let alias = resolve_alias(&config, alias.as_deref(), AliasCommand::Read)?;
let alias_name = alias.as_ref().map(|(name, _)| *name);
let alias_config = alias.as_ref().map(|(_, alias)| *alias);
let alias_graph = alias_config.and_then(|alias| alias.graph.as_deref());
let target_available = alias_graph.is_some() || config.cli_graph_name().is_some();
let (legacy_uri, alias_args) =
normalize_legacy_alias_uri(legacy_uri, target_available, alias_name, alias_args);
// `--target` is gone; resolve an alias's legacy `graph` name to its
// URI (a positional URI still wins).
let uri = match uri.or(legacy_uri) {
Some(uri) => Some(uri),
None => match alias_graph {
Some(name) => Some(config.resolve_target_uri(None, Some(name), None)?),
None => None,
},
};
let client = client::GraphClient::resolve(
&config,
cli.server.as_deref(),
cli.graph.as_deref(),
uri,
uri.or(legacy_uri),
cli.profile.as_deref(),
cli.store.as_deref(),
)?;
let query_source = resolve_query_source(
&config,
query.as_ref(),
query_string.as_deref(),
alias_config.map(|a| a.query.as_str()),
)?;
let params_json = merged_params_json(
alias_name,
alias_config
.map(|alias| alias.args.as_slice())
.unwrap_or(&[]),
&alias_args,
load_params_json(&params)?,
)?;
let target = resolve_read_target(
&config,
branch,
snapshot,
alias_config.and_then(|alias| alias.branch.clone()),
)?;
let query_name = name.or_else(|| alias_config.and_then(|alias| alias.name.clone()));
let query_source =
resolve_query_source(&config, query.as_ref(), query_string.as_deref(), None)?;
let params_json = load_params_json(&params)?;
let target = resolve_read_target(&config, branch, snapshot, None)?;
let output = client
.query(
target,
&query_source,
query_name.as_deref(),
params_json.as_ref(),
)
.query(target, &query_source, name.as_deref(), params_json.as_ref())
.await?;
let format = resolve_read_format(
&config,
format,
json,
alias_config.and_then(|alias| alias.format),
);
let format = resolve_read_format(&config, format, json, None);
print_read_output(&output, format, &config)?;
}
Command::Mutate {
uri,
legacy_uri,
config,
alias,
query,
query_string,
name,
params,
branch,
json,
alias_args,
} => {
if alias.is_none() && query.is_none() && query_string.is_none() {
bail!("exactly one of --query, --query-string, or --alias must be provided");
if query.is_none() && query_string.is_none() {
bail!("provide a mutation query: --query <file> or -e '<inline gq>'");
}
let config = load_cli_config(config.as_ref())?;
let alias = resolve_alias(&config, alias.as_deref(), AliasCommand::Change)?;
let alias_name = alias.as_ref().map(|(name, _)| *name);
let alias_config = alias.as_ref().map(|(_, alias)| *alias);
let alias_graph = alias_config.and_then(|alias| alias.graph.as_deref());
let target_available = alias_graph.is_some() || config.cli_graph_name().is_some();
let (legacy_uri, alias_args) =
normalize_legacy_alias_uri(legacy_uri, target_available, alias_name, alias_args);
// `--target` is gone; resolve an alias's legacy `graph` name to its
// URI (a positional URI still wins).
let uri = match uri.or(legacy_uri) {
Some(uri) => Some(uri),
None => match alias_graph {
Some(name) => Some(config.resolve_target_uri(None, Some(name), None)?),
None => None,
},
};
let client = client::GraphClient::resolve_with_policy(
&config,
cli.server.as_deref(),
cli.graph.as_deref(),
uri,
uri.or(legacy_uri),
cli.as_actor.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
)?;
let query_source = resolve_query_source(
&config,
query.as_ref(),
query_string.as_deref(),
alias_config.map(|a| a.query.as_str()),
)?;
let params_json = merged_params_json(
alias_name,
alias_config
.map(|alias| alias.args.as_slice())
.unwrap_or(&[]),
&alias_args,
load_params_json(&params)?,
)?;
let branch = resolve_branch(
&config,
branch,
alias_config.and_then(|alias| alias.branch.clone()),
"main",
);
let query_name = name.or_else(|| alias_config.and_then(|alias| alias.name.clone()));
let query_source =
resolve_query_source(&config, query.as_ref(), query_string.as_deref(), None)?;
let params_json = load_params_json(&params)?;
let branch = resolve_branch(&config, branch, None, "main");
let output = client
.mutate(
&branch,
&query_source,
query_name.as_deref(),
params_json.as_ref(),
)
.mutate(&branch, &query_source, name.as_deref(), params_json.as_ref())
.await?;
if json {
print_json(&output)?;
@ -761,6 +639,37 @@ async fn main() -> Result<()> {
print_change_human(&output);
}
}
Command::Alias {
name,
args,
config,
params,
format,
json,
} => {
let config = load_cli_config(config.as_ref())?;
let operator_config = crate::operator::load_operator_config()?;
let Some(operator_alias) = operator_config.aliases.get(&name) else {
let defined: Vec<&str> =
operator_config.aliases.keys().map(String::as_str).collect();
bail!(
"unknown alias '{name}'; defined aliases: [{}] \
(add it under `aliases:` in ~/.omnigraph/config.yaml)",
defined.join(", ")
);
};
let output = execute_operator_alias(
&http_client,
&config,
&name,
operator_alias,
&args,
load_params_json(&params)?,
)
.await?;
let format = resolve_read_format(&config, format, json, operator_alias.format);
print_read_output(&output, format, &config)?;
}
Command::Policy { command } => match command {
PolicyCommand::Validate { config } => {
let config = load_cli_config(config.as_ref())?;

View file

@ -106,6 +106,7 @@ pub(crate) fn command_plane(cmd: &Command) -> Plane {
match cmd {
Command::Query { .. }
| Command::Mutate { .. }
| Command::Alias { .. }
| Command::Load { .. }
| Command::Ingest { .. }
| Command::Branch { .. }
@ -168,6 +169,7 @@ pub(crate) fn command_label(cmd: &Command) -> &'static str {
Command::Commit { .. } => "commit",
Command::Query { .. } => "query",
Command::Mutate { .. } => "mutate",
Command::Alias { .. } => "alias",
Command::Policy { .. } => "policy",
Command::Optimize { .. } => "optimize",
Command::Repair { .. } => "repair",