From 5ba9656666c5512780c14a448f2ecb1c1e48d467 Mon Sep 17 00:00:00 2001 From: aaltshuler Date: Thu, 11 Jun 2026 23:34:04 +0300 Subject: [PATCH] feat(cli): init stops scaffolding omnigraph.yaml; cluster init replaces it (RFC-008 stage 3) omnigraph init no longer writes a legacy config into cwd (the source of the earlier test-pollution bug, and a scaffold for a deprecated file); the scaffolder is deleted. omnigraph cluster init scaffolds the replacement: a minimal valid cluster.yaml (version: 1, optional metadata.name / storage:, a commented graphs example), refusing to overwrite. The scaffold validates clean via cluster validate in the e2e. Co-Authored-By: Claude Fable 5 --- crates/omnigraph-cli/src/cli.rs | 17 +++++ crates/omnigraph-cli/src/helpers.rs | 63 ------------------- crates/omnigraph-cli/src/main.rs | 38 ++++++++++- crates/omnigraph-cli/tests/cli_cluster.rs | 47 ++++++++++++++ .../omnigraph-cli/tests/cli_schema_config.rs | 3 +- 5 files changed, 103 insertions(+), 65 deletions(-) diff --git a/crates/omnigraph-cli/src/cli.rs b/crates/omnigraph-cli/src/cli.rs index 7b976b4..6b89f0e 100644 --- a/crates/omnigraph-cli/src/cli.rs +++ b/crates/omnigraph-cli/src/cli.rs @@ -345,6 +345,23 @@ pub(crate) enum Command { #[derive(Debug, Subcommand)] pub(crate) enum ClusterCommand { + /// Scaffold a minimal cluster.yaml in the config directory (refuses + /// if one exists). The cluster checkout replaces the legacy + /// omnigraph.yaml scaffold (RFC-008 stage 3). + Init { + /// Directory to scaffold into (default: .) + #[arg(long, default_value = ".")] + config: PathBuf, + /// Optional deployment label (metadata.name) + #[arg(long)] + name: Option, + /// Optional storage root URI (s3://bucket/prefix); omit for the + /// config-dir layout + #[arg(long)] + storage: Option, + #[arg(long)] + json: bool, + }, /// Validate cluster.yaml and referenced schemas, queries, and policy files. Validate { /// Cluster config directory containing cluster.yaml. diff --git a/crates/omnigraph-cli/src/helpers.rs b/crates/omnigraph-cli/src/helpers.rs index a48f2e4..67fb6ea 100644 --- a/crates/omnigraph-cli/src/helpers.rs +++ b/crates/omnigraph-cli/src/helpers.rs @@ -677,69 +677,6 @@ pub(crate) fn normalize_legacy_alias_uri( (Some(candidate), alias_args) } -pub(crate) fn scaffold_config_if_missing(uri: &str) -> Result<()> { - let path = inferred_config_path(uri)?; - if path.exists() { - return Ok(()); - } - - fs::write( - path, - format!( - "\ -project: - name: Omnigraph Project - -graphs: - local: - uri: {} - # bearer_token_env: OMNIGRAPH_BEARER_TOKEN - -server: - graph: local - bind: 127.0.0.1:8080 - -cli: - graph: local - branch: main - output_format: table - table_max_column_width: 80 - table_cell_layout: truncate - -query: - roots: - - queries - - . - -aliases: - # owner: - # command: read - # query: context.gq - # name: decision_owner - # args: [slug] - # graph: local - # branch: main - # format: kv - # - # attach_trace: - # command: change - # query: mutations.gq - # name: attach_trace - # args: [decision_slug, trace_slug] - # graph: local - # branch: main - -# auth: -# env_file: ./.env.omni -# -# policy: -# file: ./policy.yaml -", - yaml_string(uri), - ), - )?; - Ok(()) -} pub(crate) fn inferred_config_path(uri: &str) -> Result { if uri.contains("://") { diff --git a/crates/omnigraph-cli/src/main.rs b/crates/omnigraph-cli/src/main.rs index 133a8a0..074ca25 100644 --- a/crates/omnigraph-cli/src/main.rs +++ b/crates/omnigraph-cli/src/main.rs @@ -153,7 +153,6 @@ async fn main() -> Result<()> { omnigraph::db::InitOptions { force }, ) .await?; - scaffold_config_if_missing(&uri)?; println!("initialized {}", uri); } Command::Load { @@ -1218,6 +1217,43 @@ async fn main() -> Result<()> { } } Command::Cluster { command } => match command { + ClusterCommand::Init { + config, + name, + storage, + json, + } => { + let target = config.join("cluster.yaml"); + if target.exists() { + bail!( + "'{}' already exists — cluster init refuses to overwrite", + target.display() + ); + } + std::fs::create_dir_all(&config)?; + let mut scaffold = String::from("version: 1\n"); + if let Some(name) = &name { + scaffold.push_str(&format!("metadata:\n name: {name}\n")); + } + match &storage { + Some(root) => scaffold.push_str(&format!("storage: {root}\n")), + None => scaffold.push_str( + "# storage: s3://bucket/prefix # omit: this folder is the storage root\n", + ), + } + scaffold.push_str( + "graphs: {}\n# graphs:\n# knowledge:\n# schema: knowledge.pg\n# queries: queries/\n", + ); + std::fs::write(&target, scaffold)?; + if json { + print_json(&serde_json::json!({ "created": target.display().to_string() }))?; + } else { + println!( + "created {} — declare graphs, then `omnigraph cluster import` and `apply`", + target.display() + ); + } + } ClusterCommand::Validate { config, json } => { let output = validate_config_dir(config); finish_cluster_validate(&output, json)?; diff --git a/crates/omnigraph-cli/tests/cli_cluster.rs b/crates/omnigraph-cli/tests/cli_cluster.rs index bfadf40..9f0326a 100644 --- a/crates/omnigraph-cli/tests/cli_cluster.rs +++ b/crates/omnigraph-cli/tests/cli_cluster.rs @@ -949,3 +949,50 @@ graphs: let leaked = b.to_string(); assert!(!leaked.contains("phantom") && !leaked.contains("9999"), "{leaked}"); } + +/// RFC-008 stage 3: `cluster init` scaffolds a minimal valid cluster.yaml +/// (the replacement for the retired omnigraph.yaml scaffold) and refuses +/// to overwrite. +#[test] +fn cluster_init_scaffolds_minimal_valid_config_and_refuses_overwrite() { + let temp = tempdir().unwrap(); + let output = cli() + .arg("cluster") + .arg("init") + .arg("--config") + .arg(temp.path()) + .arg("--name") + .arg("brain") + .output() + .unwrap(); + assert!(output.status.success(), "{output:?}"); + let text = fs::read_to_string(temp.path().join("cluster.yaml")).unwrap(); + assert!(text.contains("version: 1") && text.contains("name: brain"), "{text}"); + + // The scaffold validates clean. + let output = cli() + .arg("cluster") + .arg("validate") + .arg("--config") + .arg(temp.path()) + .arg("--json") + .output() + .unwrap(); + assert!(output.status.success(), "{output:?}"); + let payload: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(payload["ok"], true, "{payload}"); + + // Refuses a second run. + let output = cli() + .arg("cluster") + .arg("init") + .arg("--config") + .arg(temp.path()) + .output() + .unwrap(); + assert!(!output.status.success()); + assert!( + String::from_utf8_lossy(&output.stderr).contains("refuses to overwrite"), + "{output:?}" + ); +} diff --git a/crates/omnigraph-cli/tests/cli_schema_config.rs b/crates/omnigraph-cli/tests/cli_schema_config.rs index 20281f6..3e2a2b9 100644 --- a/crates/omnigraph-cli/tests/cli_schema_config.rs +++ b/crates/omnigraph-cli/tests/cli_schema_config.rs @@ -36,7 +36,8 @@ fn init_creates_graph_successfully_on_missing_local_directory() { assert!(stdout.contains("initialized")); assert!(graph.join("_schema.pg").exists()); assert!(graph.join("__manifest").exists()); - assert!(temp.path().join("omnigraph.yaml").exists()); + // RFC-008 stage 3: init no longer scaffolds the legacy config file. + assert!(!temp.path().join("omnigraph.yaml").exists()); } #[test]