icydb-cli 0.181.14

Developer CLI tools for IcyDB
//! Module: CLI config initialization.
//! Responsibility: create a default `icydb.toml` in the resolved config root.
//! Does not own: config validation, report rendering, or endpoint surface gates.
//! Boundary: receives parsed init args and writes one user-facing config file.

use std::{
    fs,
    path::{Path, PathBuf},
    process::Command,
};

use serde::Deserialize;

use crate::cli::ConfigInitArgs;

use super::resolution::{CONFIG_FILE_NAME, resolve_start_dir};

/// Create a default IcyDB config file at the repository/workspace config root.
pub(crate) fn init_config(args: ConfigInitArgs) -> Result<(), String> {
    init_config_with_existing_config_path(args, existing_config_path)
}

#[cfg(test)]
pub(crate) fn init_config_without_existing_config(args: ConfigInitArgs) -> Result<(), String> {
    init_config_with_existing_config_path(args, |_| Ok(None))
}

fn init_config_with_existing_config_path(
    args: ConfigInitArgs,
    existing_config_path: impl FnOnce(&Path) -> Result<Option<PathBuf>, String>,
) -> Result<(), String> {
    let start_dir = resolve_start_dir(args.start_dir())?;
    let path = existing_config_path(start_dir.as_path())?
        .unwrap_or_else(|| init_config_path(start_dir.as_path()));

    if path.exists() && !args.force() {
        return Err(config_exists_message(path.as_path()));
    }

    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)
            .map_err(|err| format!("create config directory '{}': {err}", parent.display()))?;
    }
    fs::write(path.as_path(), render_default_config(&args))
        .map_err(|err| format!("write IcyDB config '{}': {err}", path.display()))?;

    println!("Wrote IcyDB config: {}", path.display());

    Ok(())
}

fn init_config_path(start_dir: &Path) -> PathBuf {
    cargo_metadata_workspace_root(start_dir)
        .unwrap_or_else(|| start_dir.to_path_buf())
        .join(CONFIG_FILE_NAME)
}

fn existing_config_path(start_dir: &Path) -> Result<Option<PathBuf>, String> {
    icydb_config_build::load_resolved_icydb_toml(start_dir, &[])
        .map(|resolved| resolved.config_path().map(Path::to_path_buf))
        .map_err(|err| err.to_string())
}

fn cargo_metadata_workspace_root(start_dir: &Path) -> Option<PathBuf> {
    let output = Command::new("cargo")
        .arg("metadata")
        .arg("--no-deps")
        .arg("--format-version")
        .arg("1")
        .current_dir(start_dir)
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }

    serde_json::from_slice::<CargoMetadata>(output.stdout.as_slice())
        .ok()
        .map(|metadata| metadata.workspace_root)
}

#[derive(Deserialize)]
struct CargoMetadata {
    workspace_root: PathBuf,
}

fn config_exists_message(path: &Path) -> String {
    format!(
        "IcyDB config already exists at '{}'; pass --force to replace it",
        path.display()
    )
}

fn render_default_config(args: &ConfigInitArgs) -> String {
    let update = args.update_config_value();
    format!(
        "\
[canisters.{canister}.sql]
readonly = {readonly}
ddl = {ddl}
fixtures = {fixtures}
update = {update}

[canisters.{canister}.metrics]
enabled = {metrics}
extended = {metrics_extended}

[canisters.{canister}.snapshot]
enabled = {snapshot}

[canisters.{canister}.schema]
enabled = {schema}
",
        canister = args.canister_name(),
        readonly = args.readonly(),
        ddl = args.ddl(),
        fixtures = args.fixtures(),
        update = update,
        metrics = args.metrics(),
        metrics_extended = args.metrics_extended(),
        snapshot = args.snapshot(),
        schema = args.schema(),
    )
}