starweaver-cli 0.2.1

Command-line interface for Starweaver
Documentation
use starweaver_core::sdk_name;

use super::{auth::oauth_cli_error, render_json_lines, CliService};
use crate::{
    args::{CatalogCommand, ConfigCommand, ProfileCommand, ToolsCommand, UpdateCommand},
    config::{
        get_config_value, init_config_file, mcp_servers, tool_need_approval, ConfigScope,
        ProviderConfig,
    },
    profiles::{
        doctor_mcp_servers, list_default_tools, list_mcp_servers, list_profiles, list_skills,
        list_subagents, show_mcp_server, show_profile, show_skill, show_subagent,
    },
    CliError, CliResult,
};

impl CliService {
    pub(super) fn profile(&self, command: ProfileCommand) -> CliResult<String> {
        match command {
            ProfileCommand::List => list_profiles(&self.config)
                .iter()
                .map(|profile| serde_json::to_string(profile).map(|line| format!("{line}\n")))
                .collect::<Result<String, _>>()
                .map_err(CliError::from),
            ProfileCommand::Show { name } => show_profile(&self.config, &name),
        }
    }

    pub(super) fn update(command: &UpdateCommand) -> CliResult<String> {
        crate::launcher::update_component_with_env_options(
            &command.target,
            crate::launcher::UpdateOptions {
                dry_run: command.dry_run,
                force: command.force,
            },
        )
    }

    pub(super) fn skills(&self, command: CatalogCommand) -> CliResult<String> {
        match command {
            CatalogCommand::List => render_json_lines(&list_skills(&self.config)),
            CatalogCommand::Show { name } => show_skill(&self.config, &name),
            CatalogCommand::Doctor => Ok(format!(
                "skill_dirs={}\nskills={}\nstatus=ok\n",
                self.config
                    .skill_dirs
                    .iter()
                    .map(|path| path.display().to_string())
                    .collect::<Vec<_>>()
                    .join(":"),
                list_skills(&self.config).len()
            )),
        }
    }

    pub(super) fn subagents(&self, command: CatalogCommand) -> CliResult<String> {
        match command {
            CatalogCommand::List => render_json_lines(&list_subagents(&self.config)),
            CatalogCommand::Show { name } => show_subagent(&self.config, &name),
            CatalogCommand::Doctor => Ok(format!(
                "subagent_dirs={}\nsubagents={}\ndisabled={}\nstatus=ok\n",
                self.config
                    .subagent_dirs
                    .iter()
                    .map(|path| path.display().to_string())
                    .collect::<Vec<_>>()
                    .join(":"),
                list_subagents(&self.config).len(),
                self.config.disabled_subagents.join(",")
            )),
        }
    }

    pub(super) fn mcp(&self, command: CatalogCommand) -> CliResult<String> {
        match command {
            CatalogCommand::List => render_json_lines(&list_mcp_servers(&self.config)),
            CatalogCommand::Show { name } => show_mcp_server(&self.config, &name),
            CatalogCommand::Doctor => {
                let findings = doctor_mcp_servers(&self.config);
                let output = render_json_lines(&findings)?;
                if findings.iter().any(|finding| finding.status == "error") {
                    Err(CliError::Config(output))
                } else {
                    Ok(output)
                }
            }
        }
    }

    pub(super) fn tools(&self, command: &ToolsCommand) -> CliResult<String> {
        match command {
            ToolsCommand::List => render_json_lines(&list_default_tools(&self.config)),
            ToolsCommand::Doctor => Ok(format!(
                "tools={}\nneed_approval={}\nstatus=ok\n",
                list_default_tools(&self.config).len(),
                tool_need_approval(&self.config).join(",")
            )),
        }
    }

    pub(super) fn config(&self, command: ConfigCommand) -> CliResult<String> {
        match command {
            ConfigCommand::Init {
                global,
                project: _,
                force,
            } => {
                let scope = if global {
                    ConfigScope::Global
                } else {
                    ConfigScope::Project
                };
                let path = init_config_file(&self.config, scope, force)?;
                Ok(format!(
                    "config_path={}\nstatus=initialized\n",
                    path.display()
                ))
            }
            ConfigCommand::Get { key } => get_config_value(&self.config, &key),
            ConfigCommand::Set {
                global,
                project: _,
                key,
                value,
            } => {
                let scope = if global {
                    ConfigScope::Global
                } else {
                    ConfigScope::Project
                };
                crate::config::set_config_value(&self.config, scope, &key, &value)?;
                Ok(format!("{key}={value}\n"))
            }
        }
    }

    pub(super) fn diagnostics(&self) -> CliResult<String> {
        Ok(format!(
            "sdk={}\nworkspace_version={}\ndatabase_path={}\nfile_store_path={}\nprofile={}\ndefault_model={}\nmodel_profiles={}\nenvd_profiles={}\noauth_refresh.enabled={}\noauth_refresh.interval_seconds={}\noauth_refresh.failure_retry_seconds={}\noauth_refresh.refresh_on_startup={}\nworkspace_root={}\nenvironment_provider={}\nfiles_policy={}\nshell_enabled={}\nskills={}\nsubagents={}\nmcp_servers={}\ntools={}\ntools.need_approval={}\nprovider.openai.ready={}\nprovider.openai.api_key_env={}\nprovider.openai.base_url={}\nprovider.codex.logged_in={}\nprovider.codex.base_url={}\nprovider.anthropic.ready={}\nprovider.anthropic.api_key_env={}\nprovider.anthropic.base_url={}\nprovider.gemini.ready={}\nprovider.gemini.api_key_env={}\nprovider.gemini.base_url={}\nprovider.google-cloud.ready={}\nprovider.google-cloud.api_key_env={}\nprovider.google-cloud.auth_token_env={}\nprovider.google-cloud.project={}\nprovider.google-cloud.location={}\nprovider.google-cloud.base_url={}\nwal=true\n",
            sdk_name(),
            env!("CARGO_PKG_VERSION"),
            self.config.database_path.display(),
            self.config.file_store_path.display(),
            self.config.default_profile,
            self.config
                .default_model
                .as_ref()
                .map(|profile| profile.model_id.as_str())
                .unwrap_or_default(),
            self.config.model_profiles.len(),
            self.config.envd_profiles.len(),
            self.config.oauth_refresh.enabled,
            self.config.oauth_refresh.interval_seconds,
            self.config.oauth_refresh.failure_retry_seconds,
            self.config.oauth_refresh.refresh_on_startup,
            self.config.workspace_root.display(),
            self.config.environment_provider,
            self.config.files_policy,
            self.config.shell_enabled,
            list_skills(&self.config).len(),
            list_subagents(&self.config).len(),
            mcp_servers(&self.config).len(),
            list_default_tools(&self.config).len(),
            tool_need_approval(&self.config).join(","),
            provider_ready(&self.config.providers.openai),
            self.config.providers.openai.api_key_env.as_deref().unwrap_or_default(),
            self.config.providers.openai.base_url.as_deref().unwrap_or_default(),
            crate::oauth::OAuthStore::new(crate::oauth::OAuthStore::default_path())
                .get_provider("codex")
                .map_err(oauth_cli_error)?
                .is_some(),
            self.config.providers.codex.base_url.as_deref().unwrap_or_default(),
            provider_ready(&self.config.providers.anthropic),
            self.config.providers.anthropic.api_key_env.as_deref().unwrap_or_default(),
            self.config.providers.anthropic.base_url.as_deref().unwrap_or_default(),
            provider_ready(&self.config.providers.gemini),
            self.config.providers.gemini.api_key_env.as_deref().unwrap_or_default(),
            self.config.providers.gemini.base_url.as_deref().unwrap_or_default(),
            provider_ready(&self.config.providers.google_cloud),
            self.config.providers.google_cloud.api_key_env.as_deref().unwrap_or_default(),
            self.config.providers.google_cloud.auth_token_env.as_deref().unwrap_or_default(),
            self.config.providers.google_cloud.project.as_deref().unwrap_or_default(),
            self.config.providers.google_cloud.location.as_deref().unwrap_or_default(),
            self.config.providers.google_cloud.base_url.as_deref().unwrap_or_default()
        ))
    }
}

fn provider_ready(provider: &ProviderConfig) -> bool {
    provider.enabled
        && (env_value_present(provider.api_key_env.as_deref())
            || env_value_present(provider.auth_token_env.as_deref()))
}

fn env_value_present(name: Option<&str>) -> bool {
    name.is_some_and(|name| {
        let name = name.trim();
        !name.is_empty() && std::env::var(name).is_ok_and(|value| !value.trim().is_empty())
    })
}