diff --git a/Cargo.lock b/Cargo.lock index 994bb5e..21403b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,9 +83,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -104,9 +104,9 @@ checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -1314,9 +1314,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.58" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -1324,9 +1324,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.58" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -1336,9 +1336,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -5323,9 +5323,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] diff --git a/Cargo.toml b/Cargo.toml index 76b37e0..56cdde5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ pest = "2" pest_derive = "2" thiserror = "2" tokio = { version = "1", features = ["rt-multi-thread", "macros", "time", "net", "signal", "sync"] } -clap = { version = "4", features = ["derive"] } +clap = { version = "4.6", features = ["derive"] } serde = { version = "1", features = ["derive"] } serde_json = "1" serde_yaml = "0.9" diff --git a/crates/omnigraph-cli/src/cli.rs b/crates/omnigraph-cli/src/cli.rs index 7b976b4..670ed7c 100644 --- a/crates/omnigraph-cli/src/cli.rs +++ b/crates/omnigraph-cli/src/cli.rs @@ -9,15 +9,24 @@ pub(crate) const DEFAULT_BEARER_TOKEN_ENV: &str = "OMNIGRAPH_BEARER_TOKEN"; #[command(name = "omnigraph")] #[command(about = "Omnigraph graph database CLI")] #[command(version = env!("CARGO_PKG_VERSION"), disable_version_flag = true)] +// Subcommands are listed grouped by plane (clap renders them in declaration +// order). clap can't print labeled headings between subcommand groups, so this +// legend names the planes; the grouping is the variant order in `Command`. +#[command(after_help = "\ +COMMANDS BY PLANE:\n \ +Data — run against a graph, embedded or via --server (query, mutate, load, \ +branch, snapshot, export, commit, schema [plan: storage], graphs).\n \ +Storage — direct storage or local files; reject --server (init, optimize, \ +repair, cleanup, lint, queries [list: session]).\n \ +Control — manage a cluster directory via --config (cluster).\n \ +Session — no graph; local config & tooling (policy, embed, login, logout, \ +config, version).\n\ +See the 'Command planes' section of the CLI reference for which flags apply where.")] pub(crate) struct Cli { - /// Actor identity for direct-engine writes (MR-722). Overrides - /// `cli.actor` from `omnigraph.yaml`. When the configured policy - /// is in effect, Cedar evaluates this actor against the requested - /// action and scope; with policy configured but neither this flag - /// nor `cli.actor` set, the engine-layer footgun guard fires and - /// the write is denied (no silent bypass). Has no effect on remote - /// HTTP writes — those resolve their actor server-side from the - /// bearer token. + /// Actor id for direct-engine writes; overrides `cli.actor`. No effect on + /// remote writes (the server resolves the actor from the bearer token). + /// With a policy configured but no actor set, the write is denied — see + /// docs/user/policy.md. #[arg(long = "as", global = true, value_name = "ACTOR")] pub(crate) as_actor: Option, @@ -38,170 +47,7 @@ pub(crate) struct Cli { #[derive(Debug, Subcommand)] pub(crate) enum Command { - /// Print the CLI version - Version, - /// Store a bearer token for a named server in ~/.omnigraph/credentials - /// (0600). Token from --token or one line on stdin: - /// `echo $TOKEN | omnigraph login prod`. The keyed token applies to - /// requests whose URL matches the server's `url` in the operator - /// config's `servers:` map. - Login { - /// Server name (keys the credential; declare its url under - /// `servers:` in ~/.omnigraph/config.yaml) - name: String, - /// The token. Prefer piping via stdin over this flag (shell - /// history). - #[arg(long)] - token: Option, - #[arg(long)] - json: bool, - }, - /// Legacy-config tooling (RFC-008): split omnigraph.yaml into its - /// two destinations. - Config { - #[command(subcommand)] - command: ConfigCommand, - }, - /// Remove a named server's stored credential. Idempotent. - Logout { - name: String, - #[arg(long)] - json: bool, - }, - /// Generate, clean, or refresh explicit seed embeddings - Embed(EmbedArgs), - /// Initialize a new graph from a schema - Init { - #[arg(long)] - schema: PathBuf, - /// Graph URI (local path or s3://) - uri: String, - /// Overwrite existing schema artifacts at the URI. Without - /// this flag, init refuses to touch a URI that already holds - /// `_schema.pg`, `_schema.ir.json`, or `__schema_state.json` - /// — closes the re-init footgun (MR-668 follow-up). With the - /// flag, the operator opts in to destructive semantics. - #[arg(long)] - force: bool, - }, - /// Load data into a graph (local or remote) - Load { - /// Graph URI - uri: Option, - #[arg(long)] - target: Option, - #[arg(long)] - config: Option, - #[arg(long)] - data: PathBuf, - /// Target branch (defaults to main). Without --from it must exist. - #[arg(long)] - branch: Option, - /// Base branch to fork --branch from when it doesn't exist yet. - /// Without this flag a missing branch is an error, never a fork. - #[arg(long)] - from: Option, - /// How existing rows are handled: overwrite | append | merge. - /// Required — overwrite is destructive, so there is no default. - #[arg(long)] - mode: CliLoadMode, - #[arg(long)] - json: bool, - }, - /// Deprecated alias of `load --from ` (defaults: --mode merge, --from main) - Ingest { - /// Graph URI - uri: Option, - #[arg(long)] - target: Option, - #[arg(long)] - config: Option, - #[arg(long)] - data: PathBuf, - #[arg(long)] - branch: Option, - #[arg(long)] - from: Option, - #[arg(long, default_value = "merge")] - mode: CliLoadMode, - #[arg(long)] - json: bool, - }, - /// Branch operations - Branch { - #[command(subcommand)] - command: BranchCommand, - }, - /// Schema planning operations - Schema { - #[command(subcommand)] - command: SchemaCommand, - }, - /// Validate queries against a schema (offline) or repo (repo-backed). - /// - /// Canonical name is `lint` (matches the `omnigraph_compiler::lint` - /// module and the `OG-XXX-NNN` lint-code vocabulary). Replaces the - /// deprecated `omnigraph query lint` / `omnigraph query check` / - /// `omnigraph check` invocations — each is kept as an argv-level - /// shim that prints a one-line stderr warning and rewrites to - /// `omnigraph lint`. Aliases are deliberately *not* exposed via - /// clap's `visible_alias` because that would advertise two - /// equivalent canonical names, which agents emit interchangeably - /// (see MR-981). - Lint { - /// Graph URI - uri: Option, - #[arg(long)] - target: Option, - #[arg(long)] - config: Option, - #[arg(long)] - query: PathBuf, - #[arg(long)] - schema: Option, - #[arg(long)] - json: bool, - }, - /// Operate on the server-side stored-query registry (`queries:`). - Queries { - #[command(subcommand)] - command: QueriesCommand, - }, - /// Show graph snapshot - Snapshot { - /// Graph URI - uri: Option, - #[arg(long)] - target: Option, - #[arg(long)] - config: Option, - #[arg(long)] - branch: Option, - #[arg(long)] - json: bool, - }, - /// Export a full graph snapshot as JSONL - Export { - /// Graph URI - uri: Option, - #[arg(long)] - target: Option, - #[arg(long)] - config: Option, - #[arg(long)] - branch: Option, - #[arg(long, hide = true)] - jsonl: bool, - #[arg(long = "type")] - type_names: Vec, - #[arg(long = "table")] - table_keys: Vec, - }, - /// Commit history operations - Commit { - #[command(subcommand)] - command: CommitCommand, - }, + // ── Data plane ── run against a graph (embedded or via --server). /// Execute a read query against a branch or snapshot. /// /// Canonical read endpoint. The previous name `omnigraph read` is @@ -274,10 +120,115 @@ pub(crate) enum Command { #[arg()] alias_args: Vec, }, - /// Policy administration and diagnostics - Policy { + /// Load data into a graph (local or remote) + Load { + /// Graph URI + uri: Option, + #[arg(long)] + target: Option, + #[arg(long)] + config: Option, + #[arg(long)] + data: PathBuf, + /// Target branch (defaults to main). Without --from it must exist. + #[arg(long)] + branch: Option, + /// Base branch to fork --branch from when it doesn't exist yet. + /// Without this flag a missing branch is an error, never a fork. + #[arg(long)] + from: Option, + /// How existing rows are handled: overwrite | append | merge. + /// Required — overwrite is destructive, so there is no default. + #[arg(long)] + mode: CliLoadMode, + #[arg(long)] + json: bool, + }, + /// Deprecated alias of `load --from ` (defaults: --mode merge, --from main) + #[command(hide = true)] + Ingest { + /// Graph URI + uri: Option, + #[arg(long)] + target: Option, + #[arg(long)] + config: Option, + #[arg(long)] + data: PathBuf, + #[arg(long)] + branch: Option, + #[arg(long)] + from: Option, + #[arg(long, default_value = "merge")] + mode: CliLoadMode, + #[arg(long)] + json: bool, + }, + /// Branch operations + Branch { #[command(subcommand)] - command: PolicyCommand, + command: BranchCommand, + }, + /// Show graph snapshot + Snapshot { + /// Graph URI + uri: Option, + #[arg(long)] + target: Option, + #[arg(long)] + config: Option, + #[arg(long)] + branch: Option, + #[arg(long)] + json: bool, + }, + /// Export a full graph snapshot as JSONL + Export { + /// Graph URI + uri: Option, + #[arg(long)] + target: Option, + #[arg(long)] + config: Option, + #[arg(long)] + branch: Option, + #[arg(long, hide = true)] + jsonl: bool, + #[arg(long = "type")] + type_names: Vec, + #[arg(long = "table")] + table_keys: Vec, + }, + /// Commit history operations + Commit { + #[command(subcommand)] + command: CommitCommand, + }, + /// Schema planning operations + Schema { + #[command(subcommand)] + command: SchemaCommand, + }, + /// Manage graphs on a multi-graph server (MR-668) + Graphs { + #[command(subcommand)] + command: GraphsCommand, + }, + + // ── Storage / local graph ops ── direct storage or local files; reject --server. + /// Initialize a new graph from a schema + Init { + #[arg(long)] + schema: PathBuf, + /// Graph URI (local path or s3://) + uri: String, + /// Overwrite existing schema artifacts at the URI. Without + /// this flag, init refuses to touch a URI that already holds + /// `_schema.pg`, `_schema.ir.json`, or `__schema_state.json` + /// — closes the re-init footgun (MR-668 follow-up). With the + /// flag, the operator opts in to destructive semantics. + #[arg(long)] + force: bool, }, /// Compact small Lance fragments in every table of the graph Optimize { @@ -331,16 +282,79 @@ pub(crate) enum Command { #[arg(long)] json: bool, }, + /// Validate queries against a schema (offline) or repo (repo-backed). + /// + /// Canonical name is `lint` (matches the `omnigraph_compiler::lint` + /// module and the `OG-XXX-NNN` lint-code vocabulary). Replaces the + /// deprecated `omnigraph query lint` / `omnigraph query check` / + /// `omnigraph check` invocations — each is kept as an argv-level + /// shim that prints a one-line stderr warning and rewrites to + /// `omnigraph lint`. Aliases are deliberately *not* exposed via + /// clap's `visible_alias` because that would advertise two + /// equivalent canonical names, which agents emit interchangeably + /// (see MR-981). + Lint { + /// Graph URI + uri: Option, + #[arg(long)] + target: Option, + #[arg(long)] + config: Option, + #[arg(long)] + query: PathBuf, + #[arg(long)] + schema: Option, + #[arg(long)] + json: bool, + }, + /// Operate on the server-side stored-query registry (`queries:`). + Queries { + #[command(subcommand)] + command: QueriesCommand, + }, + + // ── Control plane ── manage a cluster directory (--config ). /// Validate and plan read-only cluster configuration. Cluster { #[command(subcommand)] command: ClusterCommand, }, - /// Manage graphs on a multi-graph server (MR-668) - Graphs { + + // ── Session / config ── no graph addressing; local tooling. + /// Policy administration and diagnostics + Policy { #[command(subcommand)] - command: GraphsCommand, + command: PolicyCommand, }, + /// Generate, clean, or refresh explicit seed embeddings + Embed(EmbedArgs), + /// Store a bearer token for a named server (0600 credentials file). Token + /// via --token or piped on stdin; see the CLI reference for token resolution. + Login { + /// Server name (keys the credential; declare its url under + /// `servers:` in ~/.omnigraph/config.yaml) + name: String, + /// The token. Prefer piping via stdin over this flag (shell + /// history). + #[arg(long)] + token: Option, + #[arg(long)] + json: bool, + }, + /// Remove a named server's stored credential. Idempotent. + Logout { + name: String, + #[arg(long)] + json: bool, + }, + /// Legacy-config tooling (RFC-008): split omnigraph.yaml into its + /// two destinations. + Config { + #[command(subcommand)] + command: ConfigCommand, + }, + /// Print the CLI version + Version, } #[derive(Debug, Subcommand)] diff --git a/crates/omnigraph-cli/tests/cli_schema_config.rs b/crates/omnigraph-cli/tests/cli_schema_config.rs index c962321..b15987f 100644 --- a/crates/omnigraph-cli/tests/cli_schema_config.rs +++ b/crates/omnigraph-cli/tests/cli_schema_config.rs @@ -24,6 +24,38 @@ fn version_command_prints_current_cli_version() { ); } +#[test] +fn help_groups_commands_by_plane() { + // RFC-010 Slice 2: `--help` clusters commands by plane (declaration order + // in the Command enum) and explains the planes in an after_help legend. + // Pinned lightly — the legend phrase + the cluster ordering — to avoid + // brittle full-text assertions on clap's help body. + let output = output_success(cli().arg("--help")); + let stdout = stdout_string(&output); + + assert!( + stdout.contains("COMMANDS BY PLANE"), + "plane legend (after_help) missing from --help:\n{stdout}" + ); + + // The Commands list precedes the legend, so first occurrences sit in the + // list and must appear in plane order: a data verb, then a storage verb, + // then the control verb. + let pos = |needle: &str| { + stdout + .find(needle) + .unwrap_or_else(|| panic!("'{needle}' not found in --help:\n{stdout}")) + }; + assert!( + pos("query") < pos("optimize"), + "data commands should be listed before storage commands" + ); + assert!( + pos("optimize") < pos("cluster"), + "storage commands should be listed before the control command" + ); +} + #[test] fn init_creates_graph_successfully_on_missing_local_directory() { let temp = tempdir().unwrap(); diff --git a/docs/user/cli-reference.md b/docs/user/cli-reference.md index 3843b2e..5be5ee3 100644 --- a/docs/user/cli-reference.md +++ b/docs/user/cli-reference.md @@ -43,6 +43,8 @@ These restrictions are enforced and reported, not silent: To maintain a server-backed graph, run the maintenance verbs from a host with storage access against the graph's storage URI (or `--target`), out-of-band from the serving process — there are no server routes for `optimize` / `repair` / `cleanup` by design. +`omnigraph --help` lists commands **clustered by plane** (data → storage → control → session) with a plane legend at the bottom. + ## Config surfaces Two config surfaces with single owners (RFC-007/RFC-008), plus a zero-config