Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/aliustech_alius.json
T

1 line
362 KiB
JSON

{"repo": "aliustech/alius", "files": {".alius/config/providers.toml": {"3d56e304e096c6e5745d05444eeed4a5d591db55": {"t": 1785745362.0709682, "n": 1016, "h": "10190fd01fa6e6d3", "c": "[router]\nstrategy = \"tiered\"\ndefault_tier = \"medium\"\nfallback_tier = \"medium\"\n\n[tiers.light]\ndescription = \"Plan Model compatibility tier.\"\nprovider = \"bigmodel\"\nmodel = \"\"\n\n[tiers.medium]\ndescription = \"Execute Model compatibility tier.\"\nprovider = \"bigmodel\"\nmodel = \"gpt-4o\"\n\n[tiers.high]\ndescription = \"Review Model compatibility tier.\"\nprovider = \"bigmodel\"\nmodel = \"\"\n\n[providers.xiaomi_mimo]\nenabled = false\nkind = \"openai-compatible\"\nbase_url = \"https://token-plan-cn.xiaomimimo.com/v1\"\napi_key_env = \"XIAOMI_MIMO_API_KEY\"\n\n[providers.deepseek]\nenabled = false\nkind = \"openai-compatible\"\nbase_url = \"https://api.deepseek.com\"\napi_key_env = \"DEEPSEEK_API_KEY\"\n\n[providers.xiaomi_mimo_anthropic]\nenabled = false\nkind = \"anthropic\"\nbase_url = \"https://token-plan-cn.xiaomimimo.com/anthropic\"\napi_key_env = \"XIAOMI_MIMO_API_KEY\"\n\n[providers.bigmodel]\nenabled = true\nkind = \"openai-compatible\"\nbase_url = \"https://open.bigmodel.cn/api/coding/paas/v4\"\napi_key_env = \"BIGMODEL_API_KEY\"\n\n[model_library]\nmodels = []\n"}, "ed9ccbca2fdad3a89ac73177ea2f8cd0ba05cb51": {"t": 1785750728.2768252, "n": 850, "h": "1e02d2858d78160b", "c": "# Model providers and routing configuration\n\n[router]\nstrategy = \"tiered\"\ndefault_tier = \"medium\"\nfallback_tier = \"medium\"\n\n[tiers.light]\ndescription = \"Plan Model compatibility tier.\"\nprovider = \"bigmodel\"\nmodel = \"\"\n\n[tiers.medium]\ndescription = \"Execute Model compatibility tier.\"\nprovider = \"bigmodel\"\nmodel = \"\"\n\n[tiers.high]\ndescription = \"Review Model compatibility tier.\"\nprovider = \"bigmodel\"\nmodel = \"\"\n\n[providers.bigmodel]\nenabled = true\nkind = \"openai-compatible\"\nbase_url = \"https://open.bigmodel.cn/api/coding/paas/v4\"\napi_key_env = \"BIGMODEL_API_KEY\"\n\n[providers.xiaomi_mimo]\nenabled = false\nkind = \"openai-compatible\"\nbase_url = \"https://api.xiaomimimo.com/v1\"\napi_key_env = \"XIAOMI_MIMO_API_KEY\"\n\n[providers.deepseek]\nenabled = false\nkind = \"openai-compatible\"\nbase_url = \"https://api.deepseek.com\"\napi_key_env = \"DEEPSEEK_API_KEY\"\n"}}, "runtime/core/config/defaults/providers.toml": {"3d56e304e096c6e5745d05444eeed4a5d591db55": {"t": 1785745369.7910736, "n": 1026, "h": "1b00f72df137dbf4", "c": "# Model providers and routing configuration\n\n[router]\nstrategy = \"tiered\"\ndefault_tier = \"medium\"\nfallback_tier = \"medium\"\n\n[tiers.light]\ndescription = \"Plan Model compatibility tier.\"\nprovider = \"bigmodel\"\nmodel = \"\"\n\n[tiers.medium]\ndescription = \"Execute Model compatibility tier.\"\nprovider = \"bigmodel\"\nmodel = \"\"\n\n[tiers.high]\ndescription = \"Review Model compatibility tier.\"\nprovider = \"bigmodel\"\nmodel = \"\"\n\n[providers.bigmodel]\nenabled = true\nkind = \"openai-compatible\"\nbase_url = \"https://open.bigmodel.cn/api/coding/paas/v4\"\napi_key_env = \"BIGMODEL_API_KEY\"\n\n[providers.xiaomi_mimo]\nenabled = false\nkind = \"openai-compatible\"\nbase_url = \"https://token-plan-cn.xiaomimimo.com/v1\"\napi_key_env = \"XIAOMI_MIMO_API_KEY\"\n\n[providers.xiaomi_mimo_anthropic]\nenabled = false\nkind = \"anthropic\"\nbase_url = \"https://token-plan-cn.xiaomimimo.com/anthropic\"\napi_key_env = \"XIAOMI_MIMO_API_KEY\"\n\n[providers.deepseek]\nenabled = false\nkind = \"openai-compatible\"\nbase_url = \"https://api.deepseek.com\"\napi_key_env = \"DEEPSEEK_API_KEY\"\n"}, "ed9ccbca2fdad3a89ac73177ea2f8cd0ba05cb51": {"t": 1785750730.8360345, "n": 850, "h": "1e02d2858d78160b", "c": "# Model providers and routing configuration\n\n[router]\nstrategy = \"tiered\"\ndefault_tier = \"medium\"\nfallback_tier = \"medium\"\n\n[tiers.light]\ndescription = \"Plan Model compatibility tier.\"\nprovider = \"bigmodel\"\nmodel = \"\"\n\n[tiers.medium]\ndescription = \"Execute Model compatibility tier.\"\nprovider = \"bigmodel\"\nmodel = \"\"\n\n[tiers.high]\ndescription = \"Review Model compatibility tier.\"\nprovider = \"bigmodel\"\nmodel = \"\"\n\n[providers.bigmodel]\nenabled = true\nkind = \"openai-compatible\"\nbase_url = \"https://open.bigmodel.cn/api/coding/paas/v4\"\napi_key_env = \"BIGMODEL_API_KEY\"\n\n[providers.xiaomi_mimo]\nenabled = false\nkind = \"openai-compatible\"\nbase_url = \"https://api.xiaomimimo.com/v1\"\napi_key_env = \"XIAOMI_MIMO_API_KEY\"\n\n[providers.deepseek]\nenabled = false\nkind = \"openai-compatible\"\nbase_url = \"https://api.deepseek.com\"\napi_key_env = \"DEEPSEEK_API_KEY\"\n"}}, "runtime/config/src/init_wizard.rs": {"3d56e304e096c6e5745d05444eeed4a5d591db55": {"t": 1785746231.8846028, "n": 65960, "h": "d1f6ae31f844d3d4", "c": "//! Pure `/init` state machine.\n//!\n//! This module owns initialization state, transitions, and renderable view\n//! data. It intentionally performs no filesystem, network, model, or SOUL IO.\n\nuse crate::{ModelAssignmentRole, ModelLibraryEntry};\nuse serde::{Deserialize, Serialize};\nuse std::path::PathBuf;\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\npub enum InitState {\n Start,\n Resume,\n CheckWorkspace,\n CreateProject,\n SelectLanguage,\n ConfigureModelPool,\n ModelSelectProvider,\n ModelInputBaseUrl,\n ModelInputApiKey,\n ModelFetchList,\n ModelImportSelect,\n ConfigureModelAssignment,\n SelectPlanModel,\n SelectExecuteModel,\n SelectReviewModel,\n ConfigureSoul,\n ResolveCapability,\n CreateWorkspace,\n Validate,\n Complete,\n Error,\n Cancelled,\n}\n\n/// The four user-configurable init stages, shown in the top-bar nav.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum InitStage {\n Language,\n ModelPool,\n Assignment,\n Soul,\n}\n\nimpl InitStage {\n pub fn all() -> [Self; 4] {\n [\n Self::Language,\n Self::ModelPool,\n Self::Assignment,\n Self::Soul,\n ]\n }\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub enum InitEvent {\n Start,\n Select(usize),\n Toggle(usize),\n Confirm,\n Back,\n Cancel,\n TextInput(String),\n AsyncOk,\n AsyncFailed(String),\n WorkspaceChecked(WorkspaceCheckResult),\n ProjectCreated,\n ModelsFetched(Vec<ModelInfo>),\n ModelsImported(Vec<String>),\n CapabilityResolved,\n WorkspaceCreated,\n ValidationPassed,\n ValidationFailed(Vec<ConfigIssue>),\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub enum InitCommand {\n None,\n CheckWorkspace,\n CreateProjectDirs {\n reset: bool,\n },\n WriteLanguageConfig {\n locale: String,\n },\n FetchModels {\n provider: String,\n api_protocol: ApiProtocol,\n base_url: String,\n api_key: String,\n },\n ImportModels(Vec<ModelInfo>),\n WriteModelAssignment {\n plan: String,\n execute: String,\n review: String,\n },\n WriteSoulConfig(String),\n ResolveCapability,\n CreateWorkspaceFromTemplate,\n ValidateConfig,\n Complete,\n Cancel,\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\npub enum ApiProtocol {\n OpenAi,\n Anthropic,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct WorkspaceCheckResult {\n pub ok: bool,\n pub git: Option<GitStatus>,\n pub message: Option<String>,\n /// True when `.alius` already existed before `check_workspace` ran.\n /// When false, the wizard auto-creates project defaults without prompting.\n pub alius_existed: bool,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct GitStatus {\n pub branch: Option<String>,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct ModelInfo {\n pub id: String,\n pub display_name: String,\n pub provider: String,\n pub api_protocol: ApiProtocol,\n pub base_url: String,\n pub model_name: String,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct SoulRef {\n pub id: String,\n pub description: String,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct ConfigIssue {\n pub message: String,\n pub section: InitConfigSection,\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\npub enum InitConfigSection {\n Language,\n ModelPool,\n ModelAssignment,\n Soul,\n Capability,\n Workspace,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub enum InitMessage {\n Info(String),\n Success(String),\n Warning(String),\n Error(String),\n Running(String),\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\npub enum CheckItemStatus {\n Done,\n Active,\n Pending,\n Warning,\n Failed,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct InitCheckItem {\n pub index: usize,\n pub title: String,\n pub status: CheckItemStatus,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct InitErrorState {\n pub source_state: InitState,\n pub message: String,\n pub recover_options: Vec<RecoverAction>,\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\npub enum RecoverAction {\n Retry,\n Back,\n Skip,\n Cancel,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct InitContext {\n pub cwd: PathBuf,\n pub git: Option<GitStatus>,\n pub language: Option<String>,\n pub selected_provider: Option<String>,\n pub api_protocol: Option<ApiProtocol>,\n pub base_url: Option<String>,\n pub api_key: Option<String>,\n pub fetched_models: Vec<ModelInfo>,\n pub selected_model_indices: Vec<usize>,\n pub model_pool: Vec<ModelInfo>,\n pub plan_model: Option<String>,\n pub execute_model: Option<String>,\n pub review_model: Option<String>,\n pub soul_choices: Vec<SoulRef>,\n pub selected_soul: Option<String>,\n pub capability_lock_exists: bool,\n pub workspace_created: bool,\n pub issues: Vec<ConfigIssue>,\n pub message_log: Vec<InitMessage>,\n pub selected_index: usize,\n pub error: Option<InitErrorState>,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct ExistingInitConfig {\n pub language: Option<String>,\n pub model_pool: Vec<ModelInfo>,\n pub plan_model: Option<String>,\n pub execute_model: Option<String>,\n pub review_model: Option<String>,\n pub selected_soul: Option<String>,\n pub soul_choices: Vec<SoulRef>,\n}\n\nimpl InitContext {\n pub fn new(cwd: PathBuf) -> Self {\n Self {\n cwd,\n git: None,\n language: None,\n selected_provider: None,\n api_protocol: None,\n base_url: None,\n api_key: None,\n fetched_models: Vec::new(),\n selected_model_indices: Vec::new(),\n model_pool: Vec::new(),\n plan_model: None,\n execute_model: None,\n review_model: None,\n soul_choices: Vec::new(),\n selected_soul: None,\n capability_lock_exists: false,\n workspace_created: false,\n issues: Vec::new(),\n message_log: Vec::new(),\n selected_index: 0,\n error: None,\n }\n }\n\n pub fn with_existing_config(cwd: PathBuf, existing: ExistingInitConfig) -> Self {\n let mut context = Self::new(cwd);\n context.language = existing.language;\n context.model_pool = existing.model_pool;\n context.plan_model = existing.plan_model;\n context.execute_model = existing.execute_model;\n context.review_model = existing.review_model;\n context.selected_soul = existing.selected_soul;\n context.soul_choices = existing.soul_choices;\n context\n }\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct InitWizard {\n pub state: InitState,\n pub context: InitContext,\n}\n\nimpl InitWizard {\n pub fn new(cwd: PathBuf) -> Self {\n Self {\n state: InitState::Start,\n context: InitContext::new(cwd),\n }\n }\n\n pub fn with_context(context: InitContext) -> Self {\n Self {\n state: InitState::Start,\n context,\n }\n }\n\n pub fn resume(mut self) -> Self {\n self.state = InitState::Resume;\n self.context.selected_index = 0;\n self\n }\n\n pub fn handle_event(&mut self, event: InitEvent) -> InitCommand {\n match event {\n InitEvent::Select(index) => {\n self.context.selected_index = index.min(self.max_selected_index());\n InitCommand::None\n }\n InitEvent::Toggle(index) => {\n self.toggle_index(index);\n InitCommand::None\n }\n InitEvent::Back => self.back(),\n InitEvent::Cancel => self.cancel(),\n InitEvent::Confirm => self.confirm(),\n InitEvent::TextInput(value) => self.text_input(value),\n InitEvent::WorkspaceChecked(result) => self.workspace_checked(result),\n InitEvent::ProjectCreated => self.project_created(),\n InitEvent::ModelsFetched(models) => self.models_fetched(models),\n InitEvent::ModelsImported(ids) => self.models_imported(ids),\n InitEvent::CapabilityResolved => self.capability_resolved(),\n InitEvent::WorkspaceCreated => self.workspace_created(),\n InitEvent::ValidationPassed => self.validation_passed(),\n InitEvent::ValidationFailed(issues) => self.validation_failed(issues),\n InitEvent::AsyncOk => self.async_ok(),\n InitEvent::AsyncFailed(message) => self.async_failed(message),\n InitEvent::Start => {\n self.state = InitState::Start;\n self.context.selected_index = 0;\n InitCommand::None\n }\n }\n }\n\n pub fn view_model(&self) -> InitViewModel {\n InitViewModel {\n header: self.header_text(),\n messages: self.render_messages(),\n check_items: self.render_check_items(),\n action_panel: self.action_panel(),\n footer: self.footer_text(),\n scope_title: self.scope_title().to_string(),\n }\n }\n\n fn confirm(&mut self) -> InitCommand {\n match self.state {\n InitState::Start => match self.selected_value().as_deref() {\n Some(\"exit\") => self.cancel(),\n _ => {\n self.state = InitState::CheckWorkspace;\n self.context.selected_index = 0;\n self.context\n .message_log\n .push(InitMessage::Running(\"Checking workspace.\".to_string()));\n InitCommand::CheckWorkspace\n }\n },\n InitState::Resume => match self.selected_value().as_deref() {\n Some(\"restart\") => {\n self.state = InitState::Start;\n self.context.message_log.clear();\n self.context.selected_index = 0;\n InitCommand::None\n }\n Some(\"exit\") => self.cancel(),\n _ => {\n self.state = self.next_unfinished_state();\n self.context.selected_index = 0;\n InitCommand::None\n }\n },\n InitState::CreateProject => match self.selected_value().as_deref() {\n Some(\"reinitialize_project\") => InitCommand::CreateProjectDirs { reset: true },\n Some(\"exit\") => self.cancel(),\n _ => self.cancel(),\n },\n InitState::SelectLanguage => match self.selected_value().as_deref() {\n Some(\"back\") => self.back(),\n Some(\"exit\") => self.cancel(),\n Some(value) => {\n let locale = if value == \"system\" {\n \"system\".to_string()\n } else {\n value.to_string()\n };\n self.context.language = Some(locale.clone());\n self.context\n .message_log\n .push(InitMessage::Success(format!(\"Language selected: {locale}\")));\n self.state = InitState::ConfigureModelPool;\n self.context.selected_index = 0;\n InitCommand::WriteLanguageConfig { locale }\n }\n None => InitCommand::None,\n },\n InitState::ConfigureModelPool => match self.selected_value().as_deref() {\n Some(\"add_model\") => {\n self.state = InitState::ModelSelectProvider;\n self.context.selected_index = 0;\n InitCommand::None\n }\n Some(\"configure_later\") | Some(\"continue_assignment\") => {\n self.state = InitState::ConfigureModelAssignment;\n self.context.selected_index = 0;\n InitCommand::None\n }\n Some(\"back\") => self.back(),\n Some(\"exit\") => self.cancel(),\n _ => InitCommand::None,\n },\n InitState::ModelSelectProvider => match self.selected_model_provider_choice() {\n Some(choice) => {\n self.context.selected_provider = Some(choice.provider.to_string());\n self.context.api_protocol = Some(choice.api_protocol);\n self.context.base_url = Some(choice.base_url.to_string());\n self.state = InitState::ModelInputBaseUrl;\n self.context.selected_index = 0;\n InitCommand::None\n }\n None => InitCommand::None,\n },\n InitState::ModelImportSelect => {\n let selected = self.selected_models_for_import();\n if selected.is_empty() {\n self.enter_error(\n InitState::ModelImportSelect,\n \"Select at least one model to import.\",\n vec![RecoverAction::Back, RecoverAction::Cancel],\n );\n InitCommand::None\n } else {\n InitCommand::ImportModels(selected)\n }\n }\n InitState::ConfigureModelAssignment => match self.selected_value().as_deref() {\n Some(\"plan\") => self.enter_assignment_state(InitState::SelectPlanModel),\n Some(\"execute\") => self.enter_assignment_state(InitState::SelectExecuteModel),\n Some(\"review\") => self.enter_assignment_state(InitState::SelectReviewModel),\n Some(\"continue_soul\") => {\n if let Some(command) = self.assignment_command() {\n self.state = InitState::ConfigureSoul;\n self.context.selected_index = 0;\n command\n } else {\n self.enter_error(\n InitState::ConfigureModelAssignment,\n \"Plan, Execute, and Review models must be assigned.\",\n vec![RecoverAction::Back, RecoverAction::Cancel],\n );\n InitCommand::None\n }\n }\n Some(\"back\") => self.back(),\n Some(\"exit\") => self.cancel(),\n _ => InitCommand::None,\n },\n InitState::SelectPlanModel\n | InitState::SelectExecuteModel\n | InitState::SelectReviewModel => match self.selected_value().as_deref() {\n Some(\"back\") => self.back(),\n Some(model_id) => {\n match self.state {\n InitState::SelectPlanModel => {\n self.context.plan_model = Some(model_id.to_string())\n }\n InitState::SelectExecuteModel => {\n self.context.execute_model = Some(model_id.to_string())\n }\n InitState::SelectReviewModel => {\n self.context.review_model = Some(model_id.to_string())\n }\n _ => {}\n }\n self.state = InitState::ConfigureModelAssignment;\n self.context.selected_index = 0;\n InitCommand::None\n }\n None => InitCommand::None,\n },\n InitState::ConfigureSoul => match self.selected_value().as_deref() {\n Some(\"back\") => self.back(),\n Some(\"exit\") => self.cancel(),\n Some(soul) => {\n self.context.selected_soul = Some(soul.to_string());\n self.context.selected_index = 0;\n InitCommand::WriteSoulConfig(soul.to_string())\n }\n None => InitCommand::None,\n },\n InitState::ResolveCapability => match self.selected_value().as_deref() {\n Some(\"skip_capability\") => {\n self.state = InitState::Complete;\n self.context.selected_index = 0;\n InitCommand::Complete\n }\n Some(\"back\") => self.back(),\n Some(\"exit\") => self.cancel(),\n _ => {\n self.state = InitState::Complete;\n self.context.selected_index = 0;\n InitCommand::Complete\n }\n },\n InitState::CreateWorkspace => match self.selected_value().as_deref() {\n Some(\"back\") => self.back(),\n Some(\"exit\") => self.cancel(),\n _ => InitCommand::CreateWorkspaceFromTemplate,\n },\n InitState::Validate => match self.selected_value().as_deref() {\n Some(\"fix_validation\") => {\n self.state = self.state_for_first_issue();\n self.context.issues.clear();\n self.context.selected_index = 0;\n InitCommand::None\n }\n Some(\"ignore_validation\") => {\n self.context.issues.clear();\n self.state = InitState::Complete;\n self.context.selected_index = 0;\n InitCommand::None\n }\n Some(\"back\") => self.back(),\n Some(\"exit\") => self.cancel(),\n _ => InitCommand::ValidateConfig,\n },\n InitState::Complete => match self.selected_value().as_deref() {\n Some(\"view_config\") => {\n self.context\n .message_log\n .push(InitMessage::Info(self.summary()));\n InitCommand::None\n }\n _ => InitCommand::Complete,\n },\n InitState::Error => self.recover(),\n InitState::CheckWorkspace\n | InitState::ModelInputBaseUrl\n | InitState::ModelInputApiKey\n | InitState::ModelFetchList\n | InitState::Cancelled => InitCommand::None,\n }\n }\n\n fn text_input(&mut self, value: String) -> InitCommand {\n match self.state {\n InitState::ModelInputBaseUrl => {\n let url = value.trim();\n if !valid_url(url) {\n self.enter_error(\n InitState::ModelInputBaseUrl,\n \"Base URL format error: missing protocol. Example: https://api.example.com/v1\",\n vec![RecoverAction::Retry, RecoverAction::Back, RecoverAction::Cancel],\n );\n InitCommand::None\n } else {\n self.context.base_url = Some(url.to_string());\n self.state = InitState::ModelInputApiKey;\n self.context.selected_index = 0;\n InitCommand::None\n }\n }\n InitState::ModelInputApiKey => {\n let api_key = value.trim().to_string();\n self.context.api_key = Some(api_key.clone());\n self.state = InitState::ModelFetchList;\n self.context\n .message_log\n .push(InitMessage::Running(\"Fetching model list.\".to_string()));\n InitCommand::FetchModels {\n provider: self.context.selected_provider.clone().unwrap_or_default(),\n api_protocol: self.context.api_protocol.unwrap_or(ApiProtocol::OpenAi),\n base_url: self.context.base_url.clone().unwrap_or_default(),\n api_key,\n }\n }\n _ => InitCommand::None,\n }\n }\n\n fn workspace_checked(&mut self, result: WorkspaceCheckResult) -> InitCommand {\n if result.ok {\n self.context.git = result.git;\n self.context.message_log.push(InitMessage::Success(\n result\n .message\n .unwrap_or_else(|| \"Workspace check complete.\".to_string()),\n ));\n // Fresh workspace: auto-create .alius without prompting.\n // Existing .alius: land on CreateProject so the user picks Reinitialize/Exit.\n if !result.alius_existed {\n self.state = InitState::CreateProject;\n self.context.selected_index = 0;\n return InitCommand::CreateProjectDirs { reset: false };\n }\n self.state = InitState::CreateProject;\n self.context.selected_index = 0;\n } else {\n self.enter_error(\n InitState::CheckWorkspace,\n result\n .message\n .unwrap_or_else(|| \"Workspace check failed.\".to_string()),\n vec![\n RecoverAction::Retry,\n RecoverAction::Back,\n RecoverAction::Cancel,\n ],\n );\n }\n InitCommand::None\n }\n\n fn project_created(&mut self) -> InitCommand {\n self.context.message_log.push(InitMessage::Success(\n \".alius project structure ready.\".to_string(),\n ));\n self.state = InitState::SelectLanguage;\n self.context.selected_index = 0;\n InitCommand::None\n }\n\n fn models_fetched(&mut self, models: Vec<ModelInfo>) -> InitCommand {\n if models.is_empty() {\n self.enter_error(\n InitState::ModelInputApiKey,\n \"No models returned. Check API Key or Base URL and try again.\",\n vec![\n RecoverAction::Retry,\n RecoverAction::Back,\n RecoverAction::Cancel,\n ],\n );\n return InitCommand::None;\n }\n let model_count = models.len();\n self.context.fetched_models = models;\n self.context.selected_model_indices = (0..self.context.fetched_models.len()).collect();\n self.context.message_log.push(InitMessage::Success(format!(\n \"Fetched {model_count} model(s).\"\n )));\n self.state = InitState::ModelImportSelect;\n self.context.selected_index = 0;\n InitCommand::None\n }\n\n fn models_imported(&mut self, ids: Vec<String>) -> InitCommand {\n for model in self\n .context\n .fetched_models\n .iter()\n .filter(|model| ids.iter().any(|id| id == &model.id))\n {\n if let Some(existing) = self\n .context\n .model_pool\n .iter_mut()\n .find(|entry| entry.id == model.id)\n {\n *existing = model.clone();\n } else {\n self.context.model_pool.push(model.clone());\n }\n }\n self.context.message_log.push(InitMessage::Success(format!(\n \"Imported {} model(s).\",\n ids.len()\n )));\n self.state = InitState::ConfigureModelAssignment;\n self.context.selected_index = 0;\n InitCommand::None\n }\n\n fn capability_resolved(&mut self) -> InitCommand {\n self.context.capability_lock_exists = true;\n self.context.message_log.push(InitMessage::Success(\n \"Capability lock generated.\".to_string(),\n ));\n self.state = InitState::Complete;\n self.context.selected_index = 0;\n InitCommand::None\n }\n\n fn workspace_created(&mut self) -> InitCommand {\n self.context.workspace_created = true;\n self.context.message_log.push(InitMessage::Success(\n \"Workspace template created.\".to_string(),\n ));\n self.state = InitState::Validate;\n self.context.selected_index = 0;\n InitCommand::None\n }\n\n fn validation_passed(&mut self) -> InitCommand {\n self.context.issues.clear();\n self.context\n .message_log\n .push(InitMessage::Success(\"Final validation passed.\".to_string()));\n self.state = InitState::Complete;\n self.context.selected_index = 0;\n InitCommand::None\n }\n\n fn validation_failed(&mut self, issues: Vec<ConfigIssue>) -> InitCommand {\n self.context.issues = issues;\n self.enter_error(\n InitState::Validate,\n \"Final validation found unfinished items.\",\n vec![\n RecoverAction::Retry,\n RecoverAction::Back,\n RecoverAction::Skip,\n RecoverAction::Cancel,\n ],\n );\n InitCommand::None\n }\n\n fn async_ok(&mut self) -> InitCommand {\n match self.state {\n InitState::CreateProject => self.project_created(),\n InitState::ConfigureSoul => {\n let soul = self\n .context\n .selected_soul\n .clone()\n .unwrap_or_else(|| \"Role\".to_string());\n self.context\n .message_log\n .push(InitMessage::Success(format!(\"Role selected: {soul}\")));\n self.state = InitState::Complete;\n self.context.selected_index = 0;\n InitCommand::Complete\n }\n InitState::ResolveCapability => self.capability_resolved(),\n InitState::CreateWorkspace => self.workspace_created(),\n _ => InitCommand::None,\n }\n }\n\n fn async_failed(&mut self, message: String) -> InitCommand {\n let source_state = match self.state {\n InitState::ModelFetchList => InitState::ModelInputApiKey,\n other => other,\n };\n self.enter_error(\n source_state,\n message,\n vec![\n RecoverAction::Retry,\n RecoverAction::Back,\n RecoverAction::Skip,\n RecoverAction::Cancel,\n ],\n );\n InitCommand::None\n }\n\n pub fn back(&mut self) -> InitCommand {\n self.state = match self.state {\n InitState::Resume => InitState::Start,\n InitState::CreateProject => InitState::Start,\n InitState::SelectLanguage => InitState::CreateProject,\n InitState::ConfigureModelPool => InitState::SelectLanguage,\n InitState::ModelSelectProvider => InitState::ConfigureModelPool,\n InitState::ModelInputBaseUrl => InitState::ModelSelectProvider,\n InitState::ModelInputApiKey => InitState::ModelInputBaseUrl,\n InitState::ModelImportSelect => InitState::ModelInputApiKey,\n InitState::ConfigureModelAssignment => InitState::ConfigureModelPool,\n InitState::SelectPlanModel\n | InitState::SelectExecuteModel\n | InitState::SelectReviewModel => InitState::ConfigureModelAssignment,\n InitState::ConfigureSoul => InitState::ConfigureModelAssignment,\n InitState::ResolveCapability => InitState::ConfigureSoul,\n InitState::CreateWorkspace | InitState::Validate => InitState::ConfigureSoul,\n InitState::Error => self\n .context\n .error\n .as_ref()\n .map(|error| previous_state(error.source_state))\n .unwrap_or(InitState::Start),\n InitState::Complete => InitState::ConfigureSoul,\n state => state,\n };\n self.context.error = None;\n self.context.selected_index = 0;\n InitCommand::None\n }\n\n fn cancel(&mut self) -> InitCommand {\n self.state = InitState::Cancelled;\n self.context.selected_index = 0;\n InitCommand::Cancel\n }\n\n fn recover(&mut self) -> InitCommand {\n let Some(error) = self.context.error.clone() else {\n self.state = InitState::Start;\n return InitCommand::None;\n };\n match self.selected_recover_action(&error) {\n Some(RecoverAction::Retry) => {\n self.state = error.source_state;\n self.context.error = None;\n self.context.selected_index = 0;\n match self.state {\n InitState::CheckWorkspace => InitCommand::CheckWorkspace,\n InitState::CreateProject => InitCommand::CreateProjectDirs { reset: false },\n InitState::ModelInputApiKey => InitCommand::None,\n InitState::ResolveCapability => InitCommand::Complete,\n InitState::CreateWorkspace => InitCommand::CreateWorkspaceFromTemplate,\n InitState::Validate => InitCommand::ValidateConfig,\n _ => InitCommand::None,\n }\n }\n Some(RecoverAction::Back) => self.back(),\n Some(RecoverAction::Skip) => {\n self.context.error = None;\n self.context.selected_index = 0;\n self.state = match error.source_state {\n InitState::ResolveCapability => InitState::Complete,\n InitState::Validate => InitState::Complete,\n _ => previous_state(error.source_state),\n };\n if self.state == InitState::Complete {\n InitCommand::Complete\n } else {\n InitCommand::None\n }\n }\n Some(RecoverAction::Cancel) | None => self.cancel(),\n }\n }\n\n fn enter_assignment_state(&mut self, state: InitState) -> InitCommand {\n self.state = state;\n self.context.selected_index = 0;\n InitCommand::None\n }\n\n fn enter_error(\n &mut self,\n source_state: InitState,\n message: impl Into<String>,\n recover_options: Vec<RecoverAction>,\n ) {\n let message = message.into();\n self.context\n .message_log\n .push(InitMessage::Error(message.clone()));\n self.context.error = Some(InitErrorState {\n source_state,\n message,\n recover_options,\n });\n self.state = InitState::Error;\n self.context.selected_index = 0;\n }\n\n fn toggle_index(&mut self, index: usize) {\n if self.state != InitState::ModelImportSelect {\n return;\n }\n if index >= self.context.fetched_models.len() {\n return;\n }\n if let Some(pos) = self\n .context\n .selected_model_indices\n .iter()\n .position(|selected| *selected == index)\n {\n self.context.selected_model_indices.remove(pos);\n } else {\n self.context.selected_model_indices.push(index);\n self.context.selected_model_indices.sort_unstable();\n }\n }\n\n fn assignment_command(&self) -> Option<InitCommand> {\n let plan = self.context.plan_model.clone()?;\n let execute = self.context.execute_model.clone()?;\n let review = self.context.review_model.clone()?;\n Some(InitCommand::WriteModelAssignment {\n plan,\n execute,\n review,\n })\n }\n\n fn state_for_first_issue(&self) -> InitState {\n self.context\n .issues\n .first()\n .map(|issue| match issue.section {\n InitConfigSection::Language => InitState::SelectLanguage,\n InitConfigSection::ModelPool => InitState::ConfigureModelPool,\n InitConfigSection::ModelAssignment => InitState::ConfigureModelAssignment,\n InitConfigSection::Soul => InitState::ConfigureSoul,\n InitConfigSection::Capability | InitConfigSection::Workspace => InitState::Complete,\n })\n .unwrap_or(InitState::Validate)\n }\n\n fn next_unfinished_state(&self) -> InitState {\n if self.context.language.is_none() {\n InitState::SelectLanguage\n } else if self.context.model_pool.is_empty() {\n InitState::ConfigureModelPool\n } else if self.assignment_command().is_none() {\n InitState::ConfigureModelAssignment\n } else if self.context.selected_soul.is_none() {\n InitState::ConfigureSoul\n } else {\n InitState::Complete\n }\n }\n\n fn selected_value(&self) -> Option<String> {\n match self.action_panel() {\n ActionPanel::SingleChoice {\n options, selected, ..\n }\n | ActionPanel::Summary {\n options, selected, ..\n } => options.get(selected).map(|option| option.value.clone()),\n _ => None,\n }\n }\n\n fn selected_recover_action(&self, error: &InitErrorState) -> Option<RecoverAction> {\n error\n .recover_options\n .get(self.context.selected_index)\n .copied()\n }\n\n fn selected_model_provider_choice(&self) -> Option<ModelProviderChoice> {\n model_provider_choices()\n .get(self.context.selected_index)\n .copied()\n }\n\n fn selected_models_for_import(&self) -> Vec<ModelInfo> {\n self.context\n .selected_model_indices\n .iter()\n .filter_map(|index| self.context.fetched_models.get(*index).cloned())\n .collect()\n }\n\n fn max_selected_index(&self) -> usize {\n match self.action_panel() {\n ActionPanel::SingleChoice { options, .. } | ActionPanel::Summary { options, .. } => {\n options.len().saturating_sub(1)\n }\n ActionPanel::MultiChoice { options, .. } => options.len().saturating_sub(1),\n _ => 0,\n }\n }\n\n fn header_text(&self) -> String {\n match self.state {\n InitState::Complete => \"Alius ✓ configuration complete\".to_string(),\n _ => \"Alius ✕ configuration incomplete\".to_string(),\n }\n }\n\n fn footer_text(&self) -> String {\n match &self.context.git {\n Some(git) => match &git.branch {\n Some(branch) => format!(\"cwd: {} git: {}\", self.context.cwd.display(), branch),\n None => format!(\"cwd: {} git\", self.context.cwd.display()),\n },\n None => format!(\"cwd: {}\", self.context.cwd.display()),\n }\n }\n\n fn render_messages(&self) -> Vec<RenderedMessage> {\n let mut messages = self\n .context\n .message_log\n .iter()\n .map(|message| match message {\n InitMessage::Info(text) => RenderedMessage::new(\"●\", text),\n InitMessage::Success(text) => RenderedMessage::new(\"✓\", text),\n InitMessage::Warning(text) => RenderedMessage::new(\"!\", text),\n InitMessage::Error(text) => RenderedMessage::new(\"!\", text),\n InitMessage::Running(text) => RenderedMessage::new(\"○\", text),\n })\n .collect::<Vec<_>>();\n if messages.is_empty() {\n messages.push(RenderedMessage::new(\n \"●\",\n \"Welcome to Alius. This workspace is not fully initialized yet.\",\n ));\n }\n messages\n }\n\n fn render_check_items(&self) -> Vec<RenderedCheckItem> {\n let active = self.checklist_state();\n INIT_CHECK_ITEMS\n .iter()\n .enumerate()\n .map(|(index, (state, title))| {\n let status = if self.state == InitState::Complete {\n CheckItemStatus::Done\n } else if *state == active {\n CheckItemStatus::Active\n } else if checklist_index(*state) < checklist_index(active) {\n CheckItemStatus::Done\n } else {\n CheckItemStatus::Pending\n };\n RenderedCheckItem {\n index: index + 1,\n title: (*title).to_string(),\n status,\n }\n })\n .collect()\n }\n\n fn action_panel(&self) -> ActionPanel {\n match self.state {\n InitState::Start => single(\n \"Start Initialization\",\n vec![\n option(\"Start Initialization\", \"start\"),\n option(\"Exit\", \"exit\"),\n ],\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n ),\n InitState::Resume => single(\n \"Detected unfinished initialization\",\n vec![\n option(\"Continue Previous\", \"continue\"),\n option(\"Restart\", \"restart\"),\n option(\"Exit\", \"exit\"),\n ],\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n ),\n InitState::CheckWorkspace | InitState::ModelFetchList => ActionPanel::None,\n InitState::CreateProject => single(\n \"Project structure\",\n vec![\n option(\"Reinitialize .alius Defaults\", \"reinitialize_project\"),\n option(\"Exit\", \"exit\"),\n ],\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n ),\n InitState::SelectLanguage => single(\n \"Choose interface language\",\n vec![\n option(\"Chinese (Simplified) - zh-CN\", \"zh-CN\"),\n option(\"English - en\", \"en\"),\n option(\"Japanese - ja\", \"ja\"),\n option(\"Follow System\", \"system\"),\n option(\"Back\", \"back\"),\n option(\"Exit\", \"exit\"),\n ],\n self.context.selected_index,\n \"Up/Down choose. Enter confirms. Esc returns.\",\n ),\n InitState::ConfigureModelPool => {\n let mut options = if self.context.model_pool.is_empty() {\n vec![\n option(\"Add Model\", \"add_model\"),\n option(\"Configure Later\", \"configure_later\"),\n ]\n } else {\n vec![\n option(\"Continue to Model Assignment\", \"continue_assignment\"),\n option(\"Add Another Model\", \"add_model\"),\n ]\n };\n options.push(option(\"Back\", \"back\"));\n options.push(option(\"Exit\", \"exit\"));\n single(\n \"Model pool\",\n options,\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n )\n }\n InitState::ModelSelectProvider => single(\n \"Choose model provider and API\",\n model_provider_choices()\n .iter()\n .map(|choice| option(choice.label, choice.value))\n .collect(),\n self.context.selected_index,\n \"Up/Down choose. Enter confirms. Esc returns.\",\n ),\n InitState::ModelInputBaseUrl => ActionPanel::TextInput {\n title: \"Enter Base URL\".to_string(),\n value: self.context.base_url.clone().unwrap_or_default(),\n placeholder: \"https://api.example.com/v1\".to_string(),\n hint: \"Enter confirms. Esc returns.\".to_string(),\n masked: false,\n },\n InitState::ModelInputApiKey => ActionPanel::TextInput {\n title: \"Enter API Key\".to_string(),\n value: String::new(),\n placeholder: \"API Key\".to_string(),\n hint: \"Enter confirms. Esc returns.\".to_string(),\n masked: false,\n },\n InitState::ModelImportSelect => ActionPanel::MultiChoice {\n title: format!(\"Found {} model(s)\", self.context.fetched_models.len()),\n options: self\n .context\n .fetched_models\n .iter()\n .enumerate()\n .map(|(index, model)| MultiChoiceOption {\n label: model.display_name.clone(),\n value: model.id.clone(),\n selected: self.context.selected_model_indices.contains(&index),\n })\n .collect(),\n highlighted: self.context.selected_index,\n hint: \"Space toggles. Enter imports selected. Esc returns.\".to_string(),\n },\n InitState::ConfigureModelAssignment => {\n let mut options = vec![\n option(\n format!(\n \"Plan Model {}\",\n self.model_label(self.context.plan_model.as_deref())\n ),\n \"plan\",\n ),\n option(\n format!(\n \"Execute Model {}\",\n self.model_label(self.context.execute_model.as_deref())\n ),\n \"execute\",\n ),\n option(\n format!(\n \"Review Model {}\",\n self.model_label(self.context.review_model.as_deref())\n ),\n \"review\",\n ),\n ];\n if self.assignment_command().is_some() {\n options.push(option(\"Continue to Role\", \"continue_soul\"));\n }\n options.push(option(\"Back\", \"back\"));\n options.push(option(\"Exit\", \"exit\"));\n single(\n \"Plan / Execute / Review\",\n options,\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n )\n }\n InitState::SelectPlanModel\n | InitState::SelectExecuteModel\n | InitState::SelectReviewModel => {\n let mut options = self\n .context\n .model_pool\n .iter()\n .map(|model| option(&model.display_name, &model.id))\n .collect::<Vec<_>>();\n options.push(option(\"Back\", \"back\"));\n single(\n self.assignment_title(),\n options,\n self.context.selected_index,\n \"Up/Down choose. Enter assigns.\",\n )\n }\n InitState::ConfigureSoul => {\n let mut options = self\n .context\n .soul_choices\n .iter()\n .map(|soul| option(format!(\"{} - {}\", soul.id, soul.description), &soul.id))\n .collect::<Vec<_>>();\n options.push(option(\"Back\", \"back\"));\n options.push(option(\"Exit\", \"exit\"));\n single(\n \"Choose Role\",\n options,\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n )\n }\n InitState::ResolveCapability => ActionPanel::None,\n InitState::CreateWorkspace => single(\n \"Create workspace\",\n vec![\n option(\"Create Workspace\", \"create_workspace\"),\n option(\"Back\", \"back\"),\n option(\"Exit\", \"exit\"),\n ],\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n ),\n InitState::Validate => {\n let options = if self.context.issues.is_empty() {\n vec![\n option(\"Run Final Validation\", \"run_validate\"),\n option(\"Back\", \"back\"),\n option(\"Exit\", \"exit\"),\n ]\n } else {\n vec![\n option(\"Return to Fix\", \"fix_validation\"),\n option(\"Ignore and Complete\", \"ignore_validation\"),\n option(\"Exit\", \"exit\"),\n ]\n };\n single(\n \"Final validation\",\n options,\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n )\n }\n InitState::Complete => ActionPanel::Summary {\n title: \"Initialization complete\".to_string(),\n lines: self.summary().lines().map(str::to_string).collect(),\n options: Vec::new(),\n selected: self.context.selected_index,\n hint: \"Working.\".to_string(),\n },\n InitState::Error => {\n let options = self\n .context\n .error\n .as_ref()\n .map(|error| {\n error\n .recover_options\n .iter()\n .map(|action| recover_option(*action))\n .collect()\n })\n .unwrap_or_else(|| vec![option(\"Exit\", \"cancel\")]);\n single(\n \"Recover\",\n options,\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n )\n }\n InitState::Cancelled => single(\n \"Cancelled\",\n vec![option(\"Exit\", \"exit\")],\n self.context.selected_index,\n \"Enter confirms.\",\n ),\n }\n }\n\n /// The configurable stage the user is currently in (for the nav bar highlight).\n /// Returns `None` for automatic/pre/post stages.\n pub fn current_stage(&self) -> Option<InitStage> {\n match self.state {\n InitState::SelectLanguage => Some(InitStage::Language),\n InitState::ConfigureModelPool\n | InitState::ModelSelectProvider\n | InitState::ModelInputBaseUrl\n | InitState::ModelInputApiKey\n | InitState::ModelFetchList\n | InitState::ModelImportSelect => Some(InitStage::ModelPool),\n InitState::ConfigureModelAssignment\n | InitState::SelectPlanModel\n | InitState::SelectExecuteModel\n | InitState::SelectReviewModel => Some(InitStage::Assignment),\n InitState::ConfigureSoul => Some(InitStage::Soul),\n _ => None,\n }\n }\n\n /// Whether a stage's data is already set (for the nav bar ✓ marker).\n pub fn stage_done(&self, stage: InitStage) -> bool {\n match stage {\n InitStage::Language => self.context.language.is_some(),\n InitStage::ModelPool => !self.context.model_pool.is_empty(),\n InitStage::Assignment => {\n self.context.plan_model.is_some()\n && self.context.execute_model.is_some()\n && self.context.review_model.is_some()\n }\n InitStage::Soul => self.context.selected_soul.is_some(),\n }\n }\n\n fn checklist_state(&self) -> InitState {\n match self.state {\n InitState::Start | InitState::Resume | InitState::CheckWorkspace => {\n InitState::CheckWorkspace\n }\n InitState::CreateProject => InitState::CreateProject,\n InitState::SelectLanguage => InitState::SelectLanguage,\n InitState::ConfigureModelPool\n | InitState::ModelSelectProvider\n | InitState::ModelInputBaseUrl\n | InitState::ModelInputApiKey\n | InitState::ModelFetchList\n | InitState::ModelImportSelect => InitState::ConfigureModelPool,\n InitState::ConfigureModelAssignment\n | InitState::SelectPlanModel\n | InitState::SelectExecuteModel\n | InitState::SelectReviewModel => InitState::ConfigureModelAssignment,\n InitState::ConfigureSoul\n | InitState::ResolveCapability\n | InitState::CreateWorkspace\n | InitState::Validate\n | InitState::Complete => InitState::ConfigureSoul,\n InitState::Error => self\n .context\n .error\n .as_ref()\n .map(|error| error.source_state)\n .unwrap_or(InitState::CheckWorkspace),\n InitState::Cancelled => InitState::CheckWorkspace,\n }\n }\n\n fn scope_title(&self) -> &'static str {\n match self.state {\n InitState::Start => \"init-start\",\n InitState::Resume => \"resume\",\n InitState::CheckWorkspace => \"check-workspace\",\n InitState::CreateProject => \"create-project\",\n InitState::SelectLanguage => \"select-language\",\n InitState::ConfigureModelPool\n | InitState::ModelSelectProvider\n | InitState::ModelInputBaseUrl\n | InitState::ModelInputApiKey\n | InitState::ModelFetchList\n | InitState::ModelImportSelect => \"configure-model-pool\",\n InitState::ConfigureModelAssignment\n | InitState::SelectPlanModel\n | InitState::SelectExecuteModel\n | InitState::SelectReviewModel => \"configure-assignment\",\n InitState::ConfigureSoul => \"configure-soul\",\n InitState::ResolveCapability => \"complete\",\n InitState::CreateWorkspace => \"create-workspace\",\n InitState::Validate => \"validate\",\n InitState::Complete => \"complete\",\n InitState::Error => \"error\",\n InitState::Cancelled => \"cancelled\",\n }\n }\n\n fn assignment_title(&self) -> &'static str {\n match self.state {\n InitState::SelectPlanModel => \"Plan Model\",\n InitState::SelectExecuteModel => \"Execute Model\",\n InitState::SelectReviewModel => \"Review Model\",\n _ => \"Model\",\n }\n }\n\n fn model_label(&self, model_id: Option<&str>) -> String {\n let Some(id) = model_id.filter(|id| !id.trim().is_empty()) else {\n return \"not configured\".to_string();\n };\n self.context\n .model_pool\n .iter()\n .find(|model| model.id == id)\n .map(|model| model.display_name.clone())\n .unwrap_or_else(|| id.to_string())\n }\n\n fn summary(&self) -> String {\n format!(\n \"Role: {}\\nPlan: {}\\nExecute: {}\\nReview: {}\",\n self.context\n .selected_soul\n .clone()\n .unwrap_or_else(|| \"not configured\".to_string()),\n self.model_label(self.context.plan_model.as_deref()),\n self.model_label(self.context.execute_model.as_deref()),\n self.model_label(self.context.review_model.as_deref()),\n )\n }\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct InitViewModel {\n pub header: String,\n pub messages: Vec<RenderedMessage>,\n pub check_items: Vec<RenderedCheckItem>,\n pub action_panel: ActionPanel,\n pub footer: String,\n pub scope_title: String,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct RenderedMessage {\n pub marker: String,\n pub text: String,\n}\n\nimpl RenderedMessage {\n fn new(marker: &str, text: impl Into<String>) -> Self {\n Self {\n marker: marker.to_string(),\n text: text.into(),\n }\n }\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct RenderedCheckItem {\n pub index: usize,\n pub title: String,\n pub status: CheckItemStatus,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub enum ActionPanel {\n SingleChoice {\n title: String,\n options: Vec<ActionOption>,\n selected: usize,\n hint: String,\n },\n MultiChoice {\n title: String,\n options: Vec<MultiChoiceOption>,\n highlighted: usize,\n hint: String,\n },\n TextInput {\n title: String,\n value: String,\n placeholder: String,\n hint: String,\n masked: bool,\n },\n Summary {\n title: String,\n lines: Vec<String>,\n options: Vec<ActionOption>,\n selected: usize,\n hint: String,\n },\n None,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct ActionOption {\n pub label: String,\n pub value: String,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct MultiChoiceOption {\n pub label: String,\n pub value: String,\n pub selected: bool,\n}\n\nconst INIT_CHECK_ITEMS: &[(InitState, &str)] = &[\n (InitState::CheckWorkspace, \"Check workspace\"),\n (InitState::CreateProject, \"Initialize .alius/\"),\n (InitState::SelectLanguage, \"Choose language\"),\n (InitState::ConfigureModelPool, \"Configure model pool\"),\n (\n InitState::ConfigureModelAssignment,\n \"Configure Plan/Execute/Review\",\n ),\n (InitState::ConfigureSoul, \"Configure Role\"),\n];\n\n#[derive(Debug, Clone, Copy)]\nstruct ModelProviderChoice {\n label: &'static str,\n value: &'static str,\n provider: &'static str,\n api_protocol: ApiProtocol,\n base_url: &'static str,\n}\n\nfn model_provider_choices() -> &'static [ModelProviderChoice] {\n &[\n ModelProviderChoice {\n label: \"BigModel GLM (Coding Plan) - OpenAI API\",\n value: \"bigmodel-openai\",\n provider: \"bigmodel\",\n api_protocol: ApiProtocol::OpenAi,\n base_url: \"https://open.bigmodel.cn/api/coding/paas/v4\",\n },\n ModelProviderChoice {\n label: \"BigModel GLM (Coding Plan) - Anthropic API\",\n value: \"bigmodel-anthropic\",\n provider: \"bigmodel\",\n api_protocol: ApiProtocol::Anthropic,\n base_url: \"https://open.bigmodel.cn/api/anthropic\",\n },\n ModelProviderChoice {\n label: \"Xiaomi MiMo (Token Plan) - OpenAI API (China)\",\n value: \"xiaomi-mimo-openai-cn\",\n provider: \"xiaomi_mimo\",\n api_protocol: ApiProtocol::OpenAi,\n base_url: \"https://token-plan-cn.xiaomimimo.com/v1\",\n },\n ModelProviderChoice {\n label: \"Xiaomi MiMo (Token Plan) - OpenAI API (Singapore)\",\n value: \"xiaomi-mimo-openai-sgp\",\n provider: \"xiaomi_mimo\",\n api_protocol: ApiProtocol::OpenAi,\n base_url: \"https://token-plan-sgp.xiaomimimo.com/v1\",\n },\n ModelProviderChoice {\n label: \"Xiaomi MiMo (Token Plan) - Anthropic API (China)\",\n value: \"xiaomi-mimo-anthropic-cn\",\n provider: \"xiaomi_mimo\",\n api_protocol: ApiProtocol::Anthropic,\n base_url: \"https://token-plan-cn.xiaomimimo.com/anthropic\",\n },\n ModelProviderChoice {\n label: \"Xiaomi MiMo (Token Plan) - Anthropic API (Singapore)\",\n value: \"xiaomi-mimo-anthropic-sgp\",\n provider: \"xiaomi_mimo\",\n api_protocol: ApiProtocol::Anthropic,\n base_url: \"https://token-plan-sgp.xiaomimimo.com/anthropic\",\n },\n ModelProviderChoice {\n label: \"DeepSeek - OpenAI API\",\n value: \"deepseek-openai\",\n provider: \"deepseek\",\n api_protocol: ApiProtocol::OpenAi,\n base_url: \"https://api.deepseek.com\",\n },\n ModelProviderChoice {\n label: \"DeepSeek - Anthropic API\",\n value: \"deepseek-anthropic\",\n provider: \"deepseek\",\n api_protocol: ApiProtocol::Anthropic,\n base_url: \"https://api.deepseek.com/anthropic\",\n },\n ]\n}\n\nfn previous_state(state: InitState) -> InitState {\n match state {\n InitState::CreateProject => InitState::Start,\n InitState::SelectLanguage => InitState::CreateProject,\n InitState::ConfigureModelPool => InitState::SelectLanguage,\n InitState::ModelSelectProvider => InitState::ConfigureModelPool,\n InitState::ModelInputBaseUrl => InitState::ModelSelectProvider,\n InitState::ModelInputApiKey => InitState::ModelInputBaseUrl,\n InitState::ModelImportSelect => InitState::ModelInputApiKey,\n InitState::ConfigureModelAssignment => InitState::ConfigureModelPool,\n InitState::SelectPlanModel\n | InitState::SelectExecuteModel\n | InitState::SelectReviewModel => InitState::ConfigureModelAssignment,\n InitState::ConfigureSoul => InitState::ConfigureModelAssignment,\n InitState::ResolveCapability | InitState::CreateWorkspace | InitState::Validate => {\n InitState::ConfigureSoul\n }\n _ => InitState::Start,\n }\n}\n\nfn checklist_index(state: InitState) -> usize {\n INIT_CHECK_ITEMS\n .iter()\n .position(|(item_state, _)| *item_state == state)\n .unwrap_or(0)\n}\n\nfn valid_url(value: &str) -> bool {\n value.starts_with(\"https://\") || value.starts_with(\"http://\")\n}\n\nfn single(\n title: impl Into<String>,\n options: Vec<ActionOption>,\n selected: usize,\n hint: impl Into<String>,\n) -> ActionPanel {\n ActionPanel::SingleChoice {\n title: title.into(),\n selected: selected.min(options.len().saturating_sub(1)),\n options,\n hint: hint.into(),\n }\n}\n\nfn option(label: impl Into<String>, value: impl Into<String>) -> ActionOption {\n ActionOption {\n label: label.into(),\n value: value.into(),\n }\n}\n\nfn recover_option(action: RecoverAction) -> ActionOption {\n match action {\n RecoverAction::Retry => option(\"Retry\", \"retry\"),\n RecoverAction::Back => option(\"Back\", \"back\"),\n RecoverAction::Skip => option(\"Skip\", \"skip\"),\n RecoverAction::Cancel => option(\"Exit\", \"cancel\"),\n }\n}\n\npub fn model_info_from_library(entry: &ModelLibraryEntry) -> ModelInfo {\n ModelInfo {\n id: entry.id.clone(),\n display_name: entry.display_name.clone(),\n provider: entry.provider.clone(),\n api_protocol: if entry.base_url.contains(\"/anthropic\") {\n ApiProtocol::Anthropic\n } else {\n ApiProtocol::OpenAi\n },\n base_url: entry.base_url.clone(),\n model_name: entry.model_name.clone(),\n }\n}\n\npub fn role_from_assignment_state(state: InitState) -> Option<ModelAssignmentRole> {\n match state {\n InitState::SelectPlanModel => Some(ModelAssignmentRole::Plan),\n InitState::SelectExecuteModel => Some(ModelAssignmentRole::Execute),\n InitState::SelectReviewModel => Some(ModelAssignmentRole::Review),\n _ => None,\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n fn wizard() -> InitWizard {\n InitWizard::new(PathBuf::from(\"/tmp/project\"))\n }\n\n #[test]\n fn start_confirm_goes_to_check_workspace() {\n let mut wizard = wizard();\n\n let command = wizard.handle_event(InitEvent::Confirm);\n\n assert_eq!(wizard.state, InitState::CheckWorkspace);\n assert_eq!(command, InitCommand::CheckWorkspace);\n }\n\n #[test]\n fn workspace_ok_goes_to_create_project() {\n let mut wizard = wizard();\n wizard.state = InitState::CheckWorkspace;\n\n let command = wizard.handle_event(InitEvent::WorkspaceChecked(WorkspaceCheckResult {\n ok: true,\n git: Some(GitStatus {\n branch: Some(\"main\".to_string()),\n }),\n message: None,\n alius_existed: true,\n }));\n\n assert_eq!(wizard.state, InitState::CreateProject);\n assert_eq!(command, InitCommand::None);\n assert!(wizard.context.git.is_some());\n }\n\n #[test]\n fn workspace_failure_enters_error() {\n let mut wizard = wizard();\n wizard.state = InitState::CheckWorkspace;\n\n wizard.handle_event(InitEvent::WorkspaceChecked(WorkspaceCheckResult {\n ok: false,\n git: None,\n message: Some(\"no permission\".to_string()),\n alius_existed: true,\n }));\n\n assert_eq!(wizard.state, InitState::Error);\n assert_eq!(\n wizard\n .context\n .error\n .as_ref()\n .map(|error| error.source_state),\n Some(InitState::CheckWorkspace)\n );\n }\n\n #[test]\n fn select_language_returns_write_command() {\n let mut wizard = wizard();\n wizard.state = InitState::SelectLanguage;\n wizard.handle_event(InitEvent::Select(1));\n\n let command = wizard.handle_event(InitEvent::Confirm);\n\n assert_eq!(wizard.state, InitState::ConfigureModelPool);\n assert_eq!(\n command,\n InitCommand::WriteLanguageConfig {\n locale: \"en\".to_string()\n }\n );\n }\n\n #[test]\n fn model_fetch_failure_enters_api_key_error() {\n let mut wizard = wizard();\n wizard.state = InitState::ModelFetchList;\n\n wizard.handle_event(InitEvent::AsyncFailed(\"bad key\".to_string()));\n\n assert_eq!(wizard.state, InitState::Error);\n assert_eq!(\n wizard\n .context\n .error\n .as_ref()\n .map(|error| error.source_state),\n Some(InitState::ModelInputApiKey)\n );\n }\n\n #[test]\n fn xiaomi_model_provider_choices_include_regions() {\n let choices = model_provider_choices()\n .iter()\n .filter(|choice| choice.provider == \"xiaomi_mimo\")\n .collect::<Vec<_>>();\n\n assert_eq!(choices.len(), 4);\n assert!(choices\n .iter()\n .any(|choice| choice.base_url == \"https://token-plan-cn.xiaomimimo.com/v1\"));\n assert!(choices\n .iter()\n .any(|choice| choice.base_url == \"https://token-plan-sgp.xiaomimimo.com/v1\"));\n assert!(choices\n .iter()\n .any(|choice| choice.base_url == \"https://token-plan-cn.xiaomimimo.com/anthropic\"));\n assert!(choices\n .iter()\n .any(|choice| choice.base_url == \"https://token-plan-sgp.xiaomimimo.com/anthropic\"));\n }\n\n #[test]\n fn models_fetched_go_to_import_select() {\n let mut wizard = wizard();\n wizard.state = InitState::ModelFetchList;\n\n wizard.handle_event(InitEvent::ModelsFetched(vec![ModelInfo {\n id: \"m1\".to_string(),\n display_name: \"m1\".to_string(),\n provider: \"bigmodel\".to_string(),\n api_protocol: ApiProtocol::OpenAi,\n base_url: \"https://example.com/v1\".to_string(),\n model_name: \"m1\".to_string(),\n }]));\n\n assert_eq!(wizard.state, InitState::ModelImportSelect);\n assert_eq!(wizard.context.selected_model_indices, vec![0]);\n }\n\n #[test]\n fn validation_passed_goes_complete() {\n let mut wizard = wizard();\n wizard.state = InitState::Validate;\n\n wizard.handle_event(InitEvent::ValidationPassed);\n\n assert_eq!(wizard.state, InitState::Complete);\n }\n\n #[test]\n fn validation_failed_enters_error() {\n let mut wizard = wizard();\n wizard.state = InitState::Validate;\n\n wizard.handle_event(InitEvent::ValidationFailed(vec![ConfigIssue {\n message: \"Review Model is not configured.\".to_string(),\n section: InitConfigSection::ModelAssignment,\n }]));\n\n assert_eq!(wizard.state, InitState::Error);\n assert_eq!(wizard.context.issues.len(), 1);\n }\n\n #[test]\n fn legacy_capability_state_completes_without_visible_step() {\n let mut wizard = wizard();\n wizard.state = InitState::ResolveCapability;\n\n let command = wizard.handle_event(InitEvent::Confirm);\n\n assert_eq!(wizard.state, InitState::Complete);\n assert_eq!(command, InitCommand::Complete);\n let titles = wizard\n .view_model()\n .check_items\n .into_iter()\n .map(|item| item.title)\n .collect::<Vec<_>>();\n assert!(!titles.iter().any(|title| title == \"Resolve Capability\"));\n assert!(!titles.iter().any(|title| title == \"Create workspace\"));\n assert!(!titles.iter().any(|title| title == \"Validate configuration\"));\n }\n\n #[test]\n fn complete_view_has_no_final_mode_choice() {\n let mut wizard = wizard();\n wizard.state = InitState::Complete;\n\n let vm = wizard.view_model();\n\n let ActionPanel::Summary { options, .. } = vm.action_panel else {\n panic!(\"expected complete summary panel\");\n };\n assert!(options.is_empty());\n }\n\n #[test]\n fn start_view_model_uses_single_choice() {\n let wizard = wizard();\n let vm = wizard.view_model();\n\n assert_eq!(vm.scope_title, \"init-start\");\n assert!(matches!(vm.action_panel, ActionPanel::SingleChoice { .. }));\n }\n\n #[test]\n fn api_key_view_model_is_plaintext() {\n let mut wizard = wizard();\n wizard.state = InitState::ModelInputApiKey;\n let vm = wizard.view_model();\n\n assert!(matches!(\n vm.action_panel,\n ActionPanel::TextInput { masked: false, .. }\n ));\n }\n}\n"}, "1b18d9542e6d3df6600a8540a65da3fd29428db4": {"t": 1785750730.3094022, "n": 64455, "h": "0cce258e2e3c2768", "c": "//! Pure `/init` state machine.\n//!\n//! This module owns initialization state, transitions, and renderable view\n//! data. It intentionally performs no filesystem, network, model, or SOUL IO.\n\nuse crate::{ModelAssignmentRole, ModelLibraryEntry};\nuse serde::{Deserialize, Serialize};\nuse std::path::PathBuf;\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\npub enum InitState {\n Start,\n Resume,\n CheckWorkspace,\n CreateProject,\n SelectLanguage,\n ConfigureModelPool,\n ModelSelectProvider,\n ModelInputBaseUrl,\n ModelInputApiKey,\n ModelFetchList,\n ModelImportSelect,\n ConfigureModelAssignment,\n SelectPlanModel,\n SelectExecuteModel,\n SelectReviewModel,\n ConfigureSoul,\n ResolveCapability,\n CreateWorkspace,\n Validate,\n Complete,\n Error,\n Cancelled,\n}\n\n/// The four user-configurable init stages, shown in the top-bar nav.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum InitStage {\n Language,\n ModelPool,\n Assignment,\n Soul,\n}\n\nimpl InitStage {\n pub fn all() -> [Self; 4] {\n [\n Self::Language,\n Self::ModelPool,\n Self::Assignment,\n Self::Soul,\n ]\n }\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub enum InitEvent {\n Start,\n Select(usize),\n Toggle(usize),\n Confirm,\n Back,\n Cancel,\n TextInput(String),\n AsyncOk,\n AsyncFailed(String),\n WorkspaceChecked(WorkspaceCheckResult),\n ProjectCreated,\n ModelsFetched(Vec<ModelInfo>),\n ModelsImported(Vec<String>),\n CapabilityResolved,\n WorkspaceCreated,\n ValidationPassed,\n ValidationFailed(Vec<ConfigIssue>),\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub enum InitCommand {\n None,\n CheckWorkspace,\n CreateProjectDirs {\n reset: bool,\n },\n WriteLanguageConfig {\n locale: String,\n },\n FetchModels {\n provider: String,\n api_protocol: ApiProtocol,\n base_url: String,\n api_key: String,\n },\n ImportModels(Vec<ModelInfo>),\n WriteModelAssignment {\n plan: String,\n execute: String,\n review: String,\n },\n WriteSoulConfig(String),\n ResolveCapability,\n CreateWorkspaceFromTemplate,\n ValidateConfig,\n Complete,\n Cancel,\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\npub enum ApiProtocol {\n OpenAi,\n Anthropic,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct WorkspaceCheckResult {\n pub ok: bool,\n pub git: Option<GitStatus>,\n pub message: Option<String>,\n /// True when `.alius` already existed before `check_workspace` ran.\n /// When false, the wizard auto-creates project defaults without prompting.\n pub alius_existed: bool,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct GitStatus {\n pub branch: Option<String>,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct ModelInfo {\n pub id: String,\n pub display_name: String,\n pub provider: String,\n pub api_protocol: ApiProtocol,\n pub base_url: String,\n pub model_name: String,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct SoulRef {\n pub id: String,\n pub description: String,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct ConfigIssue {\n pub message: String,\n pub section: InitConfigSection,\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\npub enum InitConfigSection {\n Language,\n ModelPool,\n ModelAssignment,\n Soul,\n Capability,\n Workspace,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub enum InitMessage {\n Info(String),\n Success(String),\n Warning(String),\n Error(String),\n Running(String),\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\npub enum CheckItemStatus {\n Done,\n Active,\n Pending,\n Warning,\n Failed,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct InitCheckItem {\n pub index: usize,\n pub title: String,\n pub status: CheckItemStatus,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct InitErrorState {\n pub source_state: InitState,\n pub message: String,\n pub recover_options: Vec<RecoverAction>,\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\npub enum RecoverAction {\n Retry,\n Back,\n Skip,\n Cancel,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct InitContext {\n pub cwd: PathBuf,\n pub git: Option<GitStatus>,\n pub language: Option<String>,\n pub selected_provider: Option<String>,\n pub api_protocol: Option<ApiProtocol>,\n pub base_url: Option<String>,\n pub api_key: Option<String>,\n pub fetched_models: Vec<ModelInfo>,\n pub selected_model_indices: Vec<usize>,\n pub model_pool: Vec<ModelInfo>,\n pub plan_model: Option<String>,\n pub execute_model: Option<String>,\n pub review_model: Option<String>,\n pub soul_choices: Vec<SoulRef>,\n pub selected_soul: Option<String>,\n pub capability_lock_exists: bool,\n pub workspace_created: bool,\n pub issues: Vec<ConfigIssue>,\n pub message_log: Vec<InitMessage>,\n pub selected_index: usize,\n pub error: Option<InitErrorState>,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct ExistingInitConfig {\n pub language: Option<String>,\n pub model_pool: Vec<ModelInfo>,\n pub plan_model: Option<String>,\n pub execute_model: Option<String>,\n pub review_model: Option<String>,\n pub selected_soul: Option<String>,\n pub soul_choices: Vec<SoulRef>,\n}\n\nimpl InitContext {\n pub fn new(cwd: PathBuf) -> Self {\n Self {\n cwd,\n git: None,\n language: None,\n selected_provider: None,\n api_protocol: None,\n base_url: None,\n api_key: None,\n fetched_models: Vec::new(),\n selected_model_indices: Vec::new(),\n model_pool: Vec::new(),\n plan_model: None,\n execute_model: None,\n review_model: None,\n soul_choices: Vec::new(),\n selected_soul: None,\n capability_lock_exists: false,\n workspace_created: false,\n issues: Vec::new(),\n message_log: Vec::new(),\n selected_index: 0,\n error: None,\n }\n }\n\n pub fn with_existing_config(cwd: PathBuf, existing: ExistingInitConfig) -> Self {\n let mut context = Self::new(cwd);\n context.language = existing.language;\n context.model_pool = existing.model_pool;\n context.plan_model = existing.plan_model;\n context.execute_model = existing.execute_model;\n context.review_model = existing.review_model;\n context.selected_soul = existing.selected_soul;\n context.soul_choices = existing.soul_choices;\n context\n }\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct InitWizard {\n pub state: InitState,\n pub context: InitContext,\n}\n\nimpl InitWizard {\n pub fn new(cwd: PathBuf) -> Self {\n Self {\n state: InitState::Start,\n context: InitContext::new(cwd),\n }\n }\n\n pub fn with_context(context: InitContext) -> Self {\n Self {\n state: InitState::Start,\n context,\n }\n }\n\n pub fn resume(mut self) -> Self {\n self.state = InitState::Resume;\n self.context.selected_index = 0;\n self\n }\n\n pub fn handle_event(&mut self, event: InitEvent) -> InitCommand {\n match event {\n InitEvent::Select(index) => {\n self.context.selected_index = index.min(self.max_selected_index());\n InitCommand::None\n }\n InitEvent::Toggle(index) => {\n self.toggle_index(index);\n InitCommand::None\n }\n InitEvent::Back => self.back(),\n InitEvent::Cancel => self.cancel(),\n InitEvent::Confirm => self.confirm(),\n InitEvent::TextInput(value) => self.text_input(value),\n InitEvent::WorkspaceChecked(result) => self.workspace_checked(result),\n InitEvent::ProjectCreated => self.project_created(),\n InitEvent::ModelsFetched(models) => self.models_fetched(models),\n InitEvent::ModelsImported(ids) => self.models_imported(ids),\n InitEvent::CapabilityResolved => self.capability_resolved(),\n InitEvent::WorkspaceCreated => self.workspace_created(),\n InitEvent::ValidationPassed => self.validation_passed(),\n InitEvent::ValidationFailed(issues) => self.validation_failed(issues),\n InitEvent::AsyncOk => self.async_ok(),\n InitEvent::AsyncFailed(message) => self.async_failed(message),\n InitEvent::Start => {\n self.state = InitState::Start;\n self.context.selected_index = 0;\n InitCommand::None\n }\n }\n }\n\n pub fn view_model(&self) -> InitViewModel {\n InitViewModel {\n header: self.header_text(),\n messages: self.render_messages(),\n check_items: self.render_check_items(),\n action_panel: self.action_panel(),\n footer: self.footer_text(),\n scope_title: self.scope_title().to_string(),\n }\n }\n\n fn confirm(&mut self) -> InitCommand {\n match self.state {\n InitState::Start => match self.selected_value().as_deref() {\n Some(\"exit\") => self.cancel(),\n _ => {\n self.state = InitState::CheckWorkspace;\n self.context.selected_index = 0;\n self.context\n .message_log\n .push(InitMessage::Running(\"Checking workspace.\".to_string()));\n InitCommand::CheckWorkspace\n }\n },\n InitState::Resume => match self.selected_value().as_deref() {\n Some(\"restart\") => {\n self.state = InitState::Start;\n self.context.message_log.clear();\n self.context.selected_index = 0;\n InitCommand::None\n }\n Some(\"exit\") => self.cancel(),\n _ => {\n self.state = self.next_unfinished_state();\n self.context.selected_index = 0;\n InitCommand::None\n }\n },\n InitState::CreateProject => match self.selected_value().as_deref() {\n Some(\"reinitialize_project\") => InitCommand::CreateProjectDirs { reset: true },\n Some(\"exit\") => self.cancel(),\n _ => self.cancel(),\n },\n InitState::SelectLanguage => match self.selected_value().as_deref() {\n Some(\"back\") => self.back(),\n Some(\"exit\") => self.cancel(),\n Some(value) => {\n let locale = if value == \"system\" {\n \"system\".to_string()\n } else {\n value.to_string()\n };\n self.context.language = Some(locale.clone());\n self.context\n .message_log\n .push(InitMessage::Success(format!(\"Language selected: {locale}\")));\n self.state = InitState::ConfigureModelPool;\n self.context.selected_index = 0;\n InitCommand::WriteLanguageConfig { locale }\n }\n None => InitCommand::None,\n },\n InitState::ConfigureModelPool => match self.selected_value().as_deref() {\n Some(\"add_model\") => {\n self.state = InitState::ModelSelectProvider;\n self.context.selected_index = 0;\n InitCommand::None\n }\n Some(\"configure_later\") | Some(\"continue_assignment\") => {\n self.state = InitState::ConfigureModelAssignment;\n self.context.selected_index = 0;\n InitCommand::None\n }\n Some(\"back\") => self.back(),\n Some(\"exit\") => self.cancel(),\n _ => InitCommand::None,\n },\n InitState::ModelSelectProvider => match self.selected_model_provider_choice() {\n Some(choice) => {\n self.context.selected_provider = Some(choice.provider.to_string());\n self.context.api_protocol = Some(choice.api_protocol);\n self.context.base_url = Some(choice.base_url.to_string());\n self.state = InitState::ModelInputBaseUrl;\n self.context.selected_index = 0;\n InitCommand::None\n }\n None => InitCommand::None,\n },\n InitState::ModelImportSelect => {\n let selected = self.selected_models_for_import();\n if selected.is_empty() {\n self.enter_error(\n InitState::ModelImportSelect,\n \"Select at least one model to import.\",\n vec![RecoverAction::Back, RecoverAction::Cancel],\n );\n InitCommand::None\n } else {\n InitCommand::ImportModels(selected)\n }\n }\n InitState::ConfigureModelAssignment => match self.selected_value().as_deref() {\n Some(\"plan\") => self.enter_assignment_state(InitState::SelectPlanModel),\n Some(\"execute\") => self.enter_assignment_state(InitState::SelectExecuteModel),\n Some(\"review\") => self.enter_assignment_state(InitState::SelectReviewModel),\n Some(\"continue_soul\") => {\n if let Some(command) = self.assignment_command() {\n self.state = InitState::ConfigureSoul;\n self.context.selected_index = 0;\n command\n } else {\n self.enter_error(\n InitState::ConfigureModelAssignment,\n \"Plan, Execute, and Review models must be assigned.\",\n vec![RecoverAction::Back, RecoverAction::Cancel],\n );\n InitCommand::None\n }\n }\n Some(\"back\") => self.back(),\n Some(\"exit\") => self.cancel(),\n _ => InitCommand::None,\n },\n InitState::SelectPlanModel\n | InitState::SelectExecuteModel\n | InitState::SelectReviewModel => match self.selected_value().as_deref() {\n Some(\"back\") => self.back(),\n Some(model_id) => {\n match self.state {\n InitState::SelectPlanModel => {\n self.context.plan_model = Some(model_id.to_string())\n }\n InitState::SelectExecuteModel => {\n self.context.execute_model = Some(model_id.to_string())\n }\n InitState::SelectReviewModel => {\n self.context.review_model = Some(model_id.to_string())\n }\n _ => {}\n }\n self.state = InitState::ConfigureModelAssignment;\n self.context.selected_index = 0;\n InitCommand::None\n }\n None => InitCommand::None,\n },\n InitState::ConfigureSoul => match self.selected_value().as_deref() {\n Some(\"back\") => self.back(),\n Some(\"exit\") => self.cancel(),\n Some(soul) => {\n self.context.selected_soul = Some(soul.to_string());\n self.context.selected_index = 0;\n InitCommand::WriteSoulConfig(soul.to_string())\n }\n None => InitCommand::None,\n },\n InitState::ResolveCapability => match self.selected_value().as_deref() {\n Some(\"skip_capability\") => {\n self.state = InitState::Complete;\n self.context.selected_index = 0;\n InitCommand::Complete\n }\n Some(\"back\") => self.back(),\n Some(\"exit\") => self.cancel(),\n _ => {\n self.state = InitState::Complete;\n self.context.selected_index = 0;\n InitCommand::Complete\n }\n },\n InitState::CreateWorkspace => match self.selected_value().as_deref() {\n Some(\"back\") => self.back(),\n Some(\"exit\") => self.cancel(),\n _ => InitCommand::CreateWorkspaceFromTemplate,\n },\n InitState::Validate => match self.selected_value().as_deref() {\n Some(\"fix_validation\") => {\n self.state = self.state_for_first_issue();\n self.context.issues.clear();\n self.context.selected_index = 0;\n InitCommand::None\n }\n Some(\"ignore_validation\") => {\n self.context.issues.clear();\n self.state = InitState::Complete;\n self.context.selected_index = 0;\n InitCommand::None\n }\n Some(\"back\") => self.back(),\n Some(\"exit\") => self.cancel(),\n _ => InitCommand::ValidateConfig,\n },\n InitState::Complete => match self.selected_value().as_deref() {\n Some(\"view_config\") => {\n self.context\n .message_log\n .push(InitMessage::Info(self.summary()));\n InitCommand::None\n }\n _ => InitCommand::Complete,\n },\n InitState::Error => self.recover(),\n InitState::CheckWorkspace\n | InitState::ModelInputBaseUrl\n | InitState::ModelInputApiKey\n | InitState::ModelFetchList\n | InitState::Cancelled => InitCommand::None,\n }\n }\n\n fn text_input(&mut self, value: String) -> InitCommand {\n match self.state {\n InitState::ModelInputBaseUrl => {\n let url = value.trim();\n if !valid_url(url) {\n self.enter_error(\n InitState::ModelInputBaseUrl,\n \"Base URL format error: missing protocol. Example: https://api.example.com/v1\",\n vec![RecoverAction::Retry, RecoverAction::Back, RecoverAction::Cancel],\n );\n InitCommand::None\n } else {\n self.context.base_url = Some(url.to_string());\n self.state = InitState::ModelInputApiKey;\n self.context.selected_index = 0;\n InitCommand::None\n }\n }\n InitState::ModelInputApiKey => {\n let api_key = value.trim().to_string();\n self.context.api_key = Some(api_key.clone());\n self.state = InitState::ModelFetchList;\n self.context\n .message_log\n .push(InitMessage::Running(\"Fetching model list.\".to_string()));\n InitCommand::FetchModels {\n provider: self.context.selected_provider.clone().unwrap_or_default(),\n api_protocol: self.context.api_protocol.unwrap_or(ApiProtocol::OpenAi),\n base_url: self.context.base_url.clone().unwrap_or_default(),\n api_key,\n }\n }\n _ => InitCommand::None,\n }\n }\n\n fn workspace_checked(&mut self, result: WorkspaceCheckResult) -> InitCommand {\n if result.ok {\n self.context.git = result.git;\n self.context.message_log.push(InitMessage::Success(\n result\n .message\n .unwrap_or_else(|| \"Workspace check complete.\".to_string()),\n ));\n // Fresh workspace: auto-create .alius without prompting.\n // Existing .alius: land on CreateProject so the user picks Reinitialize/Exit.\n if !result.alius_existed {\n self.state = InitState::CreateProject;\n self.context.selected_index = 0;\n return InitCommand::CreateProjectDirs { reset: false };\n }\n self.state = InitState::CreateProject;\n self.context.selected_index = 0;\n } else {\n self.enter_error(\n InitState::CheckWorkspace,\n result\n .message\n .unwrap_or_else(|| \"Workspace check failed.\".to_string()),\n vec![\n RecoverAction::Retry,\n RecoverAction::Back,\n RecoverAction::Cancel,\n ],\n );\n }\n InitCommand::None\n }\n\n fn project_created(&mut self) -> InitCommand {\n self.context.message_log.push(InitMessage::Success(\n \".alius project structure ready.\".to_string(),\n ));\n self.state = InitState::SelectLanguage;\n self.context.selected_index = 0;\n InitCommand::None\n }\n\n fn models_fetched(&mut self, models: Vec<ModelInfo>) -> InitCommand {\n if models.is_empty() {\n self.enter_error(\n InitState::ModelInputApiKey,\n \"No models returned. Check API Key or Base URL and try again.\",\n vec![\n RecoverAction::Retry,\n RecoverAction::Back,\n RecoverAction::Cancel,\n ],\n );\n return InitCommand::None;\n }\n let model_count = models.len();\n self.context.fetched_models = models;\n self.context.selected_model_indices = (0..self.context.fetched_models.len()).collect();\n self.context.message_log.push(InitMessage::Success(format!(\n \"Fetched {model_count} model(s).\"\n )));\n self.state = InitState::ModelImportSelect;\n self.context.selected_index = 0;\n InitCommand::None\n }\n\n fn models_imported(&mut self, ids: Vec<String>) -> InitCommand {\n for model in self\n .context\n .fetched_models\n .iter()\n .filter(|model| ids.iter().any(|id| id == &model.id))\n {\n if let Some(existing) = self\n .context\n .model_pool\n .iter_mut()\n .find(|entry| entry.id == model.id)\n {\n *existing = model.clone();\n } else {\n self.context.model_pool.push(model.clone());\n }\n }\n self.context.message_log.push(InitMessage::Success(format!(\n \"Imported {} model(s).\",\n ids.len()\n )));\n self.state = InitState::ConfigureModelAssignment;\n self.context.selected_index = 0;\n InitCommand::None\n }\n\n fn capability_resolved(&mut self) -> InitCommand {\n self.context.capability_lock_exists = true;\n self.context.message_log.push(InitMessage::Success(\n \"Capability lock generated.\".to_string(),\n ));\n self.state = InitState::Complete;\n self.context.selected_index = 0;\n InitCommand::None\n }\n\n fn workspace_created(&mut self) -> InitCommand {\n self.context.workspace_created = true;\n self.context.message_log.push(InitMessage::Success(\n \"Workspace template created.\".to_string(),\n ));\n self.state = InitState::Validate;\n self.context.selected_index = 0;\n InitCommand::None\n }\n\n fn validation_passed(&mut self) -> InitCommand {\n self.context.issues.clear();\n self.context\n .message_log\n .push(InitMessage::Success(\"Final validation passed.\".to_string()));\n self.state = InitState::Complete;\n self.context.selected_index = 0;\n InitCommand::None\n }\n\n fn validation_failed(&mut self, issues: Vec<ConfigIssue>) -> InitCommand {\n self.context.issues = issues;\n self.enter_error(\n InitState::Validate,\n \"Final validation found unfinished items.\",\n vec![\n RecoverAction::Retry,\n RecoverAction::Back,\n RecoverAction::Skip,\n RecoverAction::Cancel,\n ],\n );\n InitCommand::None\n }\n\n fn async_ok(&mut self) -> InitCommand {\n match self.state {\n InitState::CreateProject => self.project_created(),\n InitState::ConfigureSoul => {\n let soul = self\n .context\n .selected_soul\n .clone()\n .unwrap_or_else(|| \"Role\".to_string());\n self.context\n .message_log\n .push(InitMessage::Success(format!(\"Role selected: {soul}\")));\n self.state = InitState::Complete;\n self.context.selected_index = 0;\n InitCommand::Complete\n }\n InitState::ResolveCapability => self.capability_resolved(),\n InitState::CreateWorkspace => self.workspace_created(),\n _ => InitCommand::None,\n }\n }\n\n fn async_failed(&mut self, message: String) -> InitCommand {\n let source_state = match self.state {\n InitState::ModelFetchList => InitState::ModelInputApiKey,\n other => other,\n };\n self.enter_error(\n source_state,\n message,\n vec![\n RecoverAction::Retry,\n RecoverAction::Back,\n RecoverAction::Skip,\n RecoverAction::Cancel,\n ],\n );\n InitCommand::None\n }\n\n pub fn back(&mut self) -> InitCommand {\n self.state = match self.state {\n InitState::Resume => InitState::Start,\n InitState::CreateProject => InitState::Start,\n InitState::SelectLanguage => InitState::CreateProject,\n InitState::ConfigureModelPool => InitState::SelectLanguage,\n InitState::ModelSelectProvider => InitState::ConfigureModelPool,\n InitState::ModelInputBaseUrl => InitState::ModelSelectProvider,\n InitState::ModelInputApiKey => InitState::ModelInputBaseUrl,\n InitState::ModelImportSelect => InitState::ModelInputApiKey,\n InitState::ConfigureModelAssignment => InitState::ConfigureModelPool,\n InitState::SelectPlanModel\n | InitState::SelectExecuteModel\n | InitState::SelectReviewModel => InitState::ConfigureModelAssignment,\n InitState::ConfigureSoul => InitState::ConfigureModelAssignment,\n InitState::ResolveCapability => InitState::ConfigureSoul,\n InitState::CreateWorkspace | InitState::Validate => InitState::ConfigureSoul,\n InitState::Error => self\n .context\n .error\n .as_ref()\n .map(|error| previous_state(error.source_state))\n .unwrap_or(InitState::Start),\n InitState::Complete => InitState::ConfigureSoul,\n state => state,\n };\n self.context.error = None;\n self.context.selected_index = 0;\n InitCommand::None\n }\n\n fn cancel(&mut self) -> InitCommand {\n self.state = InitState::Cancelled;\n self.context.selected_index = 0;\n InitCommand::Cancel\n }\n\n fn recover(&mut self) -> InitCommand {\n let Some(error) = self.context.error.clone() else {\n self.state = InitState::Start;\n return InitCommand::None;\n };\n match self.selected_recover_action(&error) {\n Some(RecoverAction::Retry) => {\n self.state = error.source_state;\n self.context.error = None;\n self.context.selected_index = 0;\n match self.state {\n InitState::CheckWorkspace => InitCommand::CheckWorkspace,\n InitState::CreateProject => InitCommand::CreateProjectDirs { reset: false },\n InitState::ModelInputApiKey => InitCommand::None,\n InitState::ResolveCapability => InitCommand::Complete,\n InitState::CreateWorkspace => InitCommand::CreateWorkspaceFromTemplate,\n InitState::Validate => InitCommand::ValidateConfig,\n _ => InitCommand::None,\n }\n }\n Some(RecoverAction::Back) => self.back(),\n Some(RecoverAction::Skip) => {\n self.context.error = None;\n self.context.selected_index = 0;\n self.state = match error.source_state {\n InitState::ResolveCapability => InitState::Complete,\n InitState::Validate => InitState::Complete,\n _ => previous_state(error.source_state),\n };\n if self.state == InitState::Complete {\n InitCommand::Complete\n } else {\n InitCommand::None\n }\n }\n Some(RecoverAction::Cancel) | None => self.cancel(),\n }\n }\n\n fn enter_assignment_state(&mut self, state: InitState) -> InitCommand {\n self.state = state;\n self.context.selected_index = 0;\n InitCommand::None\n }\n\n fn enter_error(\n &mut self,\n source_state: InitState,\n message: impl Into<String>,\n recover_options: Vec<RecoverAction>,\n ) {\n let message = message.into();\n self.context\n .message_log\n .push(InitMessage::Error(message.clone()));\n self.context.error = Some(InitErrorState {\n source_state,\n message,\n recover_options,\n });\n self.state = InitState::Error;\n self.context.selected_index = 0;\n }\n\n fn toggle_index(&mut self, index: usize) {\n if self.state != InitState::ModelImportSelect {\n return;\n }\n if index >= self.context.fetched_models.len() {\n return;\n }\n if let Some(pos) = self\n .context\n .selected_model_indices\n .iter()\n .position(|selected| *selected == index)\n {\n self.context.selected_model_indices.remove(pos);\n } else {\n self.context.selected_model_indices.push(index);\n self.context.selected_model_indices.sort_unstable();\n }\n }\n\n fn assignment_command(&self) -> Option<InitCommand> {\n let plan = self.context.plan_model.clone()?;\n let execute = self.context.execute_model.clone()?;\n let review = self.context.review_model.clone()?;\n Some(InitCommand::WriteModelAssignment {\n plan,\n execute,\n review,\n })\n }\n\n fn state_for_first_issue(&self) -> InitState {\n self.context\n .issues\n .first()\n .map(|issue| match issue.section {\n InitConfigSection::Language => InitState::SelectLanguage,\n InitConfigSection::ModelPool => InitState::ConfigureModelPool,\n InitConfigSection::ModelAssignment => InitState::ConfigureModelAssignment,\n InitConfigSection::Soul => InitState::ConfigureSoul,\n InitConfigSection::Capability | InitConfigSection::Workspace => InitState::Complete,\n })\n .unwrap_or(InitState::Validate)\n }\n\n fn next_unfinished_state(&self) -> InitState {\n if self.context.language.is_none() {\n InitState::SelectLanguage\n } else if self.context.model_pool.is_empty() {\n InitState::ConfigureModelPool\n } else if self.assignment_command().is_none() {\n InitState::ConfigureModelAssignment\n } else if self.context.selected_soul.is_none() {\n InitState::ConfigureSoul\n } else {\n InitState::Complete\n }\n }\n\n fn selected_value(&self) -> Option<String> {\n match self.action_panel() {\n ActionPanel::SingleChoice {\n options, selected, ..\n }\n | ActionPanel::Summary {\n options, selected, ..\n } => options.get(selected).map(|option| option.value.clone()),\n _ => None,\n }\n }\n\n fn selected_recover_action(&self, error: &InitErrorState) -> Option<RecoverAction> {\n error\n .recover_options\n .get(self.context.selected_index)\n .copied()\n }\n\n fn selected_model_provider_choice(&self) -> Option<ModelProviderChoice> {\n model_provider_choices()\n .get(self.context.selected_index)\n .copied()\n }\n\n fn selected_models_for_import(&self) -> Vec<ModelInfo> {\n self.context\n .selected_model_indices\n .iter()\n .filter_map(|index| self.context.fetched_models.get(*index).cloned())\n .collect()\n }\n\n fn max_selected_index(&self) -> usize {\n match self.action_panel() {\n ActionPanel::SingleChoice { options, .. } | ActionPanel::Summary { options, .. } => {\n options.len().saturating_sub(1)\n }\n ActionPanel::MultiChoice { options, .. } => options.len().saturating_sub(1),\n _ => 0,\n }\n }\n\n fn header_text(&self) -> String {\n match self.state {\n InitState::Complete => \"Alius ✓ configuration complete\".to_string(),\n _ => \"Alius ✕ configuration incomplete\".to_string(),\n }\n }\n\n fn footer_text(&self) -> String {\n match &self.context.git {\n Some(git) => match &git.branch {\n Some(branch) => format!(\"cwd: {} git: {}\", self.context.cwd.display(), branch),\n None => format!(\"cwd: {} git\", self.context.cwd.display()),\n },\n None => format!(\"cwd: {}\", self.context.cwd.display()),\n }\n }\n\n fn render_messages(&self) -> Vec<RenderedMessage> {\n let mut messages = self\n .context\n .message_log\n .iter()\n .map(|message| match message {\n InitMessage::Info(text) => RenderedMessage::new(\"●\", text),\n InitMessage::Success(text) => RenderedMessage::new(\"✓\", text),\n InitMessage::Warning(text) => RenderedMessage::new(\"!\", text),\n InitMessage::Error(text) => RenderedMessage::new(\"!\", text),\n InitMessage::Running(text) => RenderedMessage::new(\"○\", text),\n })\n .collect::<Vec<_>>();\n if messages.is_empty() {\n messages.push(RenderedMessage::new(\n \"●\",\n \"Welcome to Alius. This workspace is not fully initialized yet.\",\n ));\n }\n messages\n }\n\n fn render_check_items(&self) -> Vec<RenderedCheckItem> {\n let active = self.checklist_state();\n INIT_CHECK_ITEMS\n .iter()\n .enumerate()\n .map(|(index, (state, title))| {\n let status = if self.state == InitState::Complete {\n CheckItemStatus::Done\n } else if *state == active {\n CheckItemStatus::Active\n } else if checklist_index(*state) < checklist_index(active) {\n CheckItemStatus::Done\n } else {\n CheckItemStatus::Pending\n };\n RenderedCheckItem {\n index: index + 1,\n title: (*title).to_string(),\n status,\n }\n })\n .collect()\n }\n\n fn action_panel(&self) -> ActionPanel {\n match self.state {\n InitState::Start => single(\n \"Start Initialization\",\n vec![\n option(\"Start Initialization\", \"start\"),\n option(\"Exit\", \"exit\"),\n ],\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n ),\n InitState::Resume => single(\n \"Detected unfinished initialization\",\n vec![\n option(\"Continue Previous\", \"continue\"),\n option(\"Restart\", \"restart\"),\n option(\"Exit\", \"exit\"),\n ],\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n ),\n InitState::CheckWorkspace | InitState::ModelFetchList => ActionPanel::None,\n InitState::CreateProject => single(\n \"Project structure\",\n vec![\n option(\"Reinitialize .alius Defaults\", \"reinitialize_project\"),\n option(\"Exit\", \"exit\"),\n ],\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n ),\n InitState::SelectLanguage => single(\n \"Choose interface language\",\n vec![\n option(\"Chinese (Simplified) - zh-CN\", \"zh-CN\"),\n option(\"English - en\", \"en\"),\n option(\"Japanese - ja\", \"ja\"),\n option(\"Follow System\", \"system\"),\n option(\"Back\", \"back\"),\n option(\"Exit\", \"exit\"),\n ],\n self.context.selected_index,\n \"Up/Down choose. Enter confirms. Esc returns.\",\n ),\n InitState::ConfigureModelPool => {\n let mut options = if self.context.model_pool.is_empty() {\n vec![\n option(\"Add Model\", \"add_model\"),\n option(\"Configure Later\", \"configure_later\"),\n ]\n } else {\n vec![\n option(\"Continue to Model Assignment\", \"continue_assignment\"),\n option(\"Add Another Model\", \"add_model\"),\n ]\n };\n options.push(option(\"Back\", \"back\"));\n options.push(option(\"Exit\", \"exit\"));\n single(\n \"Model pool\",\n options,\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n )\n }\n InitState::ModelSelectProvider => single(\n \"Choose model provider and API\",\n model_provider_choices()\n .iter()\n .map(|choice| option(choice.label, choice.value))\n .collect(),\n self.context.selected_index,\n \"Up/Down choose. Enter confirms. Esc returns.\",\n ),\n InitState::ModelInputBaseUrl => ActionPanel::TextInput {\n title: \"Enter Base URL\".to_string(),\n value: self.context.base_url.clone().unwrap_or_default(),\n placeholder: \"https://api.example.com/v1\".to_string(),\n hint: \"Enter confirms. Esc returns.\".to_string(),\n masked: false,\n },\n InitState::ModelInputApiKey => ActionPanel::TextInput {\n title: \"Enter API Key\".to_string(),\n value: String::new(),\n placeholder: \"API Key\".to_string(),\n hint: \"Enter confirms. Esc returns.\".to_string(),\n masked: false,\n },\n InitState::ModelImportSelect => ActionPanel::MultiChoice {\n title: format!(\"Found {} model(s)\", self.context.fetched_models.len()),\n options: self\n .context\n .fetched_models\n .iter()\n .enumerate()\n .map(|(index, model)| MultiChoiceOption {\n label: model.display_name.clone(),\n value: model.id.clone(),\n selected: self.context.selected_model_indices.contains(&index),\n })\n .collect(),\n highlighted: self.context.selected_index,\n hint: \"Space toggles. Enter imports selected. Esc returns.\".to_string(),\n },\n InitState::ConfigureModelAssignment => {\n let mut options = vec![\n option(\n format!(\n \"Plan Model {}\",\n self.model_label(self.context.plan_model.as_deref())\n ),\n \"plan\",\n ),\n option(\n format!(\n \"Execute Model {}\",\n self.model_label(self.context.execute_model.as_deref())\n ),\n \"execute\",\n ),\n option(\n format!(\n \"Review Model {}\",\n self.model_label(self.context.review_model.as_deref())\n ),\n \"review\",\n ),\n ];\n if self.assignment_command().is_some() {\n options.push(option(\"Continue to Role\", \"continue_soul\"));\n }\n options.push(option(\"Back\", \"back\"));\n options.push(option(\"Exit\", \"exit\"));\n single(\n \"Plan / Execute / Review\",\n options,\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n )\n }\n InitState::SelectPlanModel\n | InitState::SelectExecuteModel\n | InitState::SelectReviewModel => {\n let mut options = self\n .context\n .model_pool\n .iter()\n .map(|model| option(&model.display_name, &model.id))\n .collect::<Vec<_>>();\n options.push(option(\"Back\", \"back\"));\n single(\n self.assignment_title(),\n options,\n self.context.selected_index,\n \"Up/Down choose. Enter assigns.\",\n )\n }\n InitState::ConfigureSoul => {\n let mut options = self\n .context\n .soul_choices\n .iter()\n .map(|soul| option(format!(\"{} - {}\", soul.id, soul.description), &soul.id))\n .collect::<Vec<_>>();\n options.push(option(\"Back\", \"back\"));\n options.push(option(\"Exit\", \"exit\"));\n single(\n \"Choose Role\",\n options,\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n )\n }\n InitState::ResolveCapability => ActionPanel::None,\n InitState::CreateWorkspace => single(\n \"Create workspace\",\n vec![\n option(\"Create Workspace\", \"create_workspace\"),\n option(\"Back\", \"back\"),\n option(\"Exit\", \"exit\"),\n ],\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n ),\n InitState::Validate => {\n let options = if self.context.issues.is_empty() {\n vec![\n option(\"Run Final Validation\", \"run_validate\"),\n option(\"Back\", \"back\"),\n option(\"Exit\", \"exit\"),\n ]\n } else {\n vec![\n option(\"Return to Fix\", \"fix_validation\"),\n option(\"Ignore and Complete\", \"ignore_validation\"),\n option(\"Exit\", \"exit\"),\n ]\n };\n single(\n \"Final validation\",\n options,\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n )\n }\n InitState::Complete => ActionPanel::Summary {\n title: \"Initialization complete\".to_string(),\n lines: self.summary().lines().map(str::to_string).collect(),\n options: Vec::new(),\n selected: self.context.selected_index,\n hint: \"Working.\".to_string(),\n },\n InitState::Error => {\n let options = self\n .context\n .error\n .as_ref()\n .map(|error| {\n error\n .recover_options\n .iter()\n .map(|action| recover_option(*action))\n .collect()\n })\n .unwrap_or_else(|| vec![option(\"Exit\", \"cancel\")]);\n single(\n \"Recover\",\n options,\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n )\n }\n InitState::Cancelled => single(\n \"Cancelled\",\n vec![option(\"Exit\", \"exit\")],\n self.context.selected_index,\n \"Enter confirms.\",\n ),\n }\n }\n\n /// The configurable stage the user is currently in (for the nav bar highlight).\n /// Returns `None` for automatic/pre/post stages.\n pub fn current_stage(&self) -> Option<InitStage> {\n match self.state {\n InitState::SelectLanguage => Some(InitStage::Language),\n InitState::ConfigureModelPool\n | InitState::ModelSelectProvider\n | InitState::ModelInputBaseUrl\n | InitState::ModelInputApiKey\n | InitState::ModelFetchList\n | InitState::ModelImportSelect => Some(InitStage::ModelPool),\n InitState::ConfigureModelAssignment\n | InitState::SelectPlanModel\n | InitState::SelectExecuteModel\n | InitState::SelectReviewModel => Some(InitStage::Assignment),\n InitState::ConfigureSoul => Some(InitStage::Soul),\n _ => None,\n }\n }\n\n /// Whether a stage's data is already set (for the nav bar ✓ marker).\n pub fn stage_done(&self, stage: InitStage) -> bool {\n match stage {\n InitStage::Language => self.context.language.is_some(),\n InitStage::ModelPool => !self.context.model_pool.is_empty(),\n InitStage::Assignment => {\n self.context.plan_model.is_some()\n && self.context.execute_model.is_some()\n && self.context.review_model.is_some()\n }\n InitStage::Soul => self.context.selected_soul.is_some(),\n }\n }\n\n fn checklist_state(&self) -> InitState {\n match self.state {\n InitState::Start | InitState::Resume | InitState::CheckWorkspace => {\n InitState::CheckWorkspace\n }\n InitState::CreateProject => InitState::CreateProject,\n InitState::SelectLanguage => InitState::SelectLanguage,\n InitState::ConfigureModelPool\n | InitState::ModelSelectProvider\n | InitState::ModelInputBaseUrl\n | InitState::ModelInputApiKey\n | InitState::ModelFetchList\n | InitState::ModelImportSelect => InitState::ConfigureModelPool,\n InitState::ConfigureModelAssignment\n | InitState::SelectPlanModel\n | InitState::SelectExecuteModel\n | InitState::SelectReviewModel => InitState::ConfigureModelAssignment,\n InitState::ConfigureSoul\n | InitState::ResolveCapability\n | InitState::CreateWorkspace\n | InitState::Validate\n | InitState::Complete => InitState::ConfigureSoul,\n InitState::Error => self\n .context\n .error\n .as_ref()\n .map(|error| error.source_state)\n .unwrap_or(InitState::CheckWorkspace),\n InitState::Cancelled => InitState::CheckWorkspace,\n }\n }\n\n fn scope_title(&self) -> &'static str {\n match self.state {\n InitState::Start => \"init-start\",\n InitState::Resume => \"resume\",\n InitState::CheckWorkspace => \"check-workspace\",\n InitState::CreateProject => \"create-project\",\n InitState::SelectLanguage => \"select-language\",\n InitState::ConfigureModelPool\n | InitState::ModelSelectProvider\n | InitState::ModelInputBaseUrl\n | InitState::ModelInputApiKey\n | InitState::ModelFetchList\n | InitState::ModelImportSelect => \"configure-model-pool\",\n InitState::ConfigureModelAssignment\n | InitState::SelectPlanModel\n | InitState::SelectExecuteModel\n | InitState::SelectReviewModel => \"configure-assignment\",\n InitState::ConfigureSoul => \"configure-soul\",\n InitState::ResolveCapability => \"complete\",\n InitState::CreateWorkspace => \"create-workspace\",\n InitState::Validate => \"validate\",\n InitState::Complete => \"complete\",\n InitState::Error => \"error\",\n InitState::Cancelled => \"cancelled\",\n }\n }\n\n fn assignment_title(&self) -> &'static str {\n match self.state {\n InitState::SelectPlanModel => \"Plan Model\",\n InitState::SelectExecuteModel => \"Execute Model\",\n InitState::SelectReviewModel => \"Review Model\",\n _ => \"Model\",\n }\n }\n\n fn model_label(&self, model_id: Option<&str>) -> String {\n let Some(id) = model_id.filter(|id| !id.trim().is_empty()) else {\n return \"not configured\".to_string();\n };\n self.context\n .model_pool\n .iter()\n .find(|model| model.id == id)\n .map(|model| model.display_name.clone())\n .unwrap_or_else(|| id.to_string())\n }\n\n fn summary(&self) -> String {\n format!(\n \"Role: {}\\nPlan: {}\\nExecute: {}\\nReview: {}\",\n self.context\n .selected_soul\n .clone()\n .unwrap_or_else(|| \"not configured\".to_string()),\n self.model_label(self.context.plan_model.as_deref()),\n self.model_label(self.context.execute_model.as_deref()),\n self.model_label(self.context.review_model.as_deref()),\n )\n }\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct InitViewModel {\n pub header: String,\n pub messages: Vec<RenderedMessage>,\n pub check_items: Vec<RenderedCheckItem>,\n pub action_panel: ActionPanel,\n pub footer: String,\n pub scope_title: String,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct RenderedMessage {\n pub marker: String,\n pub text: String,\n}\n\nimpl RenderedMessage {\n fn new(marker: &str, text: impl Into<String>) -> Self {\n Self {\n marker: marker.to_string(),\n text: text.into(),\n }\n }\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct RenderedCheckItem {\n pub index: usize,\n pub title: String,\n pub status: CheckItemStatus,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub enum ActionPanel {\n SingleChoice {\n title: String,\n options: Vec<ActionOption>,\n selected: usize,\n hint: String,\n },\n MultiChoice {\n title: String,\n options: Vec<MultiChoiceOption>,\n highlighted: usize,\n hint: String,\n },\n TextInput {\n title: String,\n value: String,\n placeholder: String,\n hint: String,\n masked: bool,\n },\n Summary {\n title: String,\n lines: Vec<String>,\n options: Vec<ActionOption>,\n selected: usize,\n hint: String,\n },\n None,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct ActionOption {\n pub label: String,\n pub value: String,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct MultiChoiceOption {\n pub label: String,\n pub value: String,\n pub selected: bool,\n}\n\nconst INIT_CHECK_ITEMS: &[(InitState, &str)] = &[\n (InitState::CheckWorkspace, \"Check workspace\"),\n (InitState::CreateProject, \"Initialize .alius/\"),\n (InitState::SelectLanguage, \"Choose language\"),\n (InitState::ConfigureModelPool, \"Configure model pool\"),\n (\n InitState::ConfigureModelAssignment,\n \"Configure Plan/Execute/Review\",\n ),\n (InitState::ConfigureSoul, \"Configure Role\"),\n];\n\n#[derive(Debug, Clone, Copy)]\nstruct ModelProviderChoice {\n label: &'static str,\n value: &'static str,\n provider: &'static str,\n api_protocol: ApiProtocol,\n base_url: &'static str,\n}\n\nfn model_provider_choices() -> &'static [ModelProviderChoice] {\n &[\n ModelProviderChoice {\n label: \"BigModel GLM (Coding Plan) - OpenAI API\",\n value: \"bigmodel-openai\",\n provider: \"bigmodel\",\n api_protocol: ApiProtocol::OpenAi,\n base_url: \"https://open.bigmodel.cn/api/coding/paas/v4\",\n },\n ModelProviderChoice {\n label: \"BigModel GLM (Coding Plan) - Anthropic API\",\n value: \"bigmodel-anthropic\",\n provider: \"bigmodel\",\n api_protocol: ApiProtocol::Anthropic,\n base_url: \"https://open.bigmodel.cn/api/anthropic\",\n },\n ModelProviderChoice {\n label: \"Xiaomi MiMo (Token Plan) - OpenAI API\",\n value: \"xiaomi-mimo-openai\",\n provider: \"xiaomi_mimo\",\n api_protocol: ApiProtocol::OpenAi,\n base_url: \"https://api.xiaomimimo.com/v1\",\n },\n ModelProviderChoice {\n label: \"Xiaomi MiMo (Token Plan) - Anthropic API\",\n value: \"xiaomi-mimo-anthropic\",\n provider: \"xiaomi_mimo\",\n api_protocol: ApiProtocol::Anthropic,\n base_url: \"https://api.xiaomimimo.com/anthropic\",\n },\n ModelProviderChoice {\n label: \"DeepSeek - OpenAI API\",\n value: \"deepseek-openai\",\n provider: \"deepseek\",\n api_protocol: ApiProtocol::OpenAi,\n base_url: \"https://api.deepseek.com\",\n },\n ModelProviderChoice {\n label: \"DeepSeek - Anthropic API\",\n value: \"deepseek-anthropic\",\n provider: \"deepseek\",\n api_protocol: ApiProtocol::Anthropic,\n base_url: \"https://api.deepseek.com/anthropic\",\n },\n ]\n}\n\nfn previous_state(state: InitState) -> InitState {\n match state {\n InitState::CreateProject => InitState::Start,\n InitState::SelectLanguage => InitState::CreateProject,\n InitState::ConfigureModelPool => InitState::SelectLanguage,\n InitState::ModelSelectProvider => InitState::ConfigureModelPool,\n InitState::ModelInputBaseUrl => InitState::ModelSelectProvider,\n InitState::ModelInputApiKey => InitState::ModelInputBaseUrl,\n InitState::ModelImportSelect => InitState::ModelInputApiKey,\n InitState::ConfigureModelAssignment => InitState::ConfigureModelPool,\n InitState::SelectPlanModel\n | InitState::SelectExecuteModel\n | InitState::SelectReviewModel => InitState::ConfigureModelAssignment,\n InitState::ConfigureSoul => InitState::ConfigureModelAssignment,\n InitState::ResolveCapability | InitState::CreateWorkspace | InitState::Validate => {\n InitState::ConfigureSoul\n }\n _ => InitState::Start,\n }\n}\n\nfn checklist_index(state: InitState) -> usize {\n INIT_CHECK_ITEMS\n .iter()\n .position(|(item_state, _)| *item_state == state)\n .unwrap_or(0)\n}\n\nfn valid_url(value: &str) -> bool {\n value.starts_with(\"https://\") || value.starts_with(\"http://\")\n}\n\nfn single(\n title: impl Into<String>,\n options: Vec<ActionOption>,\n selected: usize,\n hint: impl Into<String>,\n) -> ActionPanel {\n ActionPanel::SingleChoice {\n title: title.into(),\n selected: selected.min(options.len().saturating_sub(1)),\n options,\n hint: hint.into(),\n }\n}\n\nfn option(label: impl Into<String>, value: impl Into<String>) -> ActionOption {\n ActionOption {\n label: label.into(),\n value: value.into(),\n }\n}\n\nfn recover_option(action: RecoverAction) -> ActionOption {\n match action {\n RecoverAction::Retry => option(\"Retry\", \"retry\"),\n RecoverAction::Back => option(\"Back\", \"back\"),\n RecoverAction::Skip => option(\"Skip\", \"skip\"),\n RecoverAction::Cancel => option(\"Exit\", \"cancel\"),\n }\n}\n\npub fn model_info_from_library(entry: &ModelLibraryEntry) -> ModelInfo {\n ModelInfo {\n id: entry.id.clone(),\n display_name: entry.display_name.clone(),\n provider: entry.provider.clone(),\n api_protocol: if entry.base_url.contains(\"/anthropic\") {\n ApiProtocol::Anthropic\n } else {\n ApiProtocol::OpenAi\n },\n base_url: entry.base_url.clone(),\n model_name: entry.model_name.clone(),\n }\n}\n\npub fn role_from_assignment_state(state: InitState) -> Option<ModelAssignmentRole> {\n match state {\n InitState::SelectPlanModel => Some(ModelAssignmentRole::Plan),\n InitState::SelectExecuteModel => Some(ModelAssignmentRole::Execute),\n InitState::SelectReviewModel => Some(ModelAssignmentRole::Review),\n _ => None,\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n fn wizard() -> InitWizard {\n InitWizard::new(PathBuf::from(\"/tmp/project\"))\n }\n\n #[test]\n fn start_confirm_goes_to_check_workspace() {\n let mut wizard = wizard();\n\n let command = wizard.handle_event(InitEvent::Confirm);\n\n assert_eq!(wizard.state, InitState::CheckWorkspace);\n assert_eq!(command, InitCommand::CheckWorkspace);\n }\n\n #[test]\n fn workspace_ok_goes_to_create_project() {\n let mut wizard = wizard();\n wizard.state = InitState::CheckWorkspace;\n\n let command = wizard.handle_event(InitEvent::WorkspaceChecked(WorkspaceCheckResult {\n ok: true,\n git: Some(GitStatus {\n branch: Some(\"main\".to_string()),\n }),\n message: None,\n alius_existed: true,\n }));\n\n assert_eq!(wizard.state, InitState::CreateProject);\n assert_eq!(command, InitCommand::None);\n assert!(wizard.context.git.is_some());\n }\n\n #[test]\n fn workspace_failure_enters_error() {\n let mut wizard = wizard();\n wizard.state = InitState::CheckWorkspace;\n\n wizard.handle_event(InitEvent::WorkspaceChecked(WorkspaceCheckResult {\n ok: false,\n git: None,\n message: Some(\"no permission\".to_string()),\n alius_existed: true,\n }));\n\n assert_eq!(wizard.state, InitState::Error);\n assert_eq!(\n wizard\n .context\n .error\n .as_ref()\n .map(|error| error.source_state),\n Some(InitState::CheckWorkspace)\n );\n }\n\n #[test]\n fn select_language_returns_write_command() {\n let mut wizard = wizard();\n wizard.state = InitState::SelectLanguage;\n wizard.handle_event(InitEvent::Select(1));\n\n let command = wizard.handle_event(InitEvent::Confirm);\n\n assert_eq!(wizard.state, InitState::ConfigureModelPool);\n assert_eq!(\n command,\n InitCommand::WriteLanguageConfig {\n locale: \"en\".to_string()\n }\n );\n }\n\n #[test]\n fn model_fetch_failure_enters_api_key_error() {\n let mut wizard = wizard();\n wizard.state = InitState::ModelFetchList;\n\n wizard.handle_event(InitEvent::AsyncFailed(\"bad key\".to_string()));\n\n assert_eq!(wizard.state, InitState::Error);\n assert_eq!(\n wizard\n .context\n .error\n .as_ref()\n .map(|error| error.source_state),\n Some(InitState::ModelInputApiKey)\n );\n }\n\n #[test]\n fn models_fetched_go_to_import_select() {\n let mut wizard = wizard();\n wizard.state = InitState::ModelFetchList;\n\n wizard.handle_event(InitEvent::ModelsFetched(vec![ModelInfo {\n id: \"m1\".to_string(),\n display_name: \"m1\".to_string(),\n provider: \"bigmodel\".to_string(),\n api_protocol: ApiProtocol::OpenAi,\n base_url: \"https://example.com/v1\".to_string(),\n model_name: \"m1\".to_string(),\n }]));\n\n assert_eq!(wizard.state, InitState::ModelImportSelect);\n assert_eq!(wizard.context.selected_model_indices, vec![0]);\n }\n\n #[test]\n fn validation_passed_goes_complete() {\n let mut wizard = wizard();\n wizard.state = InitState::Validate;\n\n wizard.handle_event(InitEvent::ValidationPassed);\n\n assert_eq!(wizard.state, InitState::Complete);\n }\n\n #[test]\n fn validation_failed_enters_error() {\n let mut wizard = wizard();\n wizard.state = InitState::Validate;\n\n wizard.handle_event(InitEvent::ValidationFailed(vec![ConfigIssue {\n message: \"Review Model is not configured.\".to_string(),\n section: InitConfigSection::ModelAssignment,\n }]));\n\n assert_eq!(wizard.state, InitState::Error);\n assert_eq!(wizard.context.issues.len(), 1);\n }\n\n #[test]\n fn legacy_capability_state_completes_without_visible_step() {\n let mut wizard = wizard();\n wizard.state = InitState::ResolveCapability;\n\n let command = wizard.handle_event(InitEvent::Confirm);\n\n assert_eq!(wizard.state, InitState::Complete);\n assert_eq!(command, InitCommand::Complete);\n let titles = wizard\n .view_model()\n .check_items\n .into_iter()\n .map(|item| item.title)\n .collect::<Vec<_>>();\n assert!(!titles.iter().any(|title| title == \"Resolve Capability\"));\n assert!(!titles.iter().any(|title| title == \"Create workspace\"));\n assert!(!titles.iter().any(|title| title == \"Validate configuration\"));\n }\n\n #[test]\n fn complete_view_has_no_final_mode_choice() {\n let mut wizard = wizard();\n wizard.state = InitState::Complete;\n\n let vm = wizard.view_model();\n\n let ActionPanel::Summary { options, .. } = vm.action_panel else {\n panic!(\"expected complete summary panel\");\n };\n assert!(options.is_empty());\n }\n\n #[test]\n fn start_view_model_uses_single_choice() {\n let wizard = wizard();\n let vm = wizard.view_model();\n\n assert_eq!(vm.scope_title, \"init-start\");\n assert!(matches!(vm.action_panel, ActionPanel::SingleChoice { .. }));\n }\n\n #[test]\n fn api_key_view_model_is_plaintext() {\n let mut wizard = wizard();\n wizard.state = InitState::ModelInputApiKey;\n let vm = wizard.view_model();\n\n assert!(matches!(\n vm.action_panel,\n ActionPanel::TextInput { masked: false, .. }\n ));\n }\n}\n"}, "677c533054f89bdd32f0ed36327b392a92dfaab6": {"t": 1785750731.7185736, "n": 64307, "h": "0d9e041e0d45379c", "c": "//! Pure `/init` state machine.\n//!\n//! This module owns initialization state, transitions, and renderable view\n//! data. It intentionally performs no filesystem, network, model, or SOUL IO.\n\nuse crate::{ModelAssignmentRole, ModelLibraryEntry};\nuse serde::{Deserialize, Serialize};\nuse std::path::PathBuf;\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\npub enum InitState {\n Start,\n Resume,\n CheckWorkspace,\n CreateProject,\n SelectLanguage,\n ConfigureModelPool,\n ModelSelectProvider,\n ModelInputBaseUrl,\n ModelInputApiKey,\n ModelFetchList,\n ModelImportSelect,\n ConfigureModelAssignment,\n SelectPlanModel,\n SelectExecuteModel,\n SelectReviewModel,\n ConfigureSoul,\n ResolveCapability,\n CreateWorkspace,\n Validate,\n Complete,\n Error,\n Cancelled,\n}\n\n/// The four user-configurable init stages, shown in the top-bar nav.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum InitStage {\n Language,\n ModelPool,\n Assignment,\n Soul,\n}\n\nimpl InitStage {\n pub fn all() -> [Self; 4] {\n [\n Self::Language,\n Self::ModelPool,\n Self::Assignment,\n Self::Soul,\n ]\n }\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub enum InitEvent {\n Start,\n Select(usize),\n Toggle(usize),\n Confirm,\n Back,\n Cancel,\n TextInput(String),\n AsyncOk,\n AsyncFailed(String),\n WorkspaceChecked(WorkspaceCheckResult),\n ProjectCreated,\n ModelsFetched(Vec<ModelInfo>),\n ModelsImported(Vec<String>),\n CapabilityResolved,\n WorkspaceCreated,\n ValidationPassed,\n ValidationFailed(Vec<ConfigIssue>),\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub enum InitCommand {\n None,\n CheckWorkspace,\n CreateProjectDirs {\n reset: bool,\n },\n WriteLanguageConfig {\n locale: String,\n },\n FetchModels {\n provider: String,\n api_protocol: ApiProtocol,\n base_url: String,\n api_key: String,\n },\n ImportModels(Vec<ModelInfo>),\n WriteModelAssignment {\n plan: String,\n execute: String,\n review: String,\n },\n WriteSoulConfig(String),\n ResolveCapability,\n CreateWorkspaceFromTemplate,\n ValidateConfig,\n Complete,\n Cancel,\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\npub enum ApiProtocol {\n OpenAi,\n Anthropic,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct WorkspaceCheckResult {\n pub ok: bool,\n pub git: Option<GitStatus>,\n pub message: Option<String>,\n /// True when `.alius` already existed before `check_workspace` ran.\n /// When false, the wizard auto-creates project defaults without prompting.\n pub alius_existed: bool,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct GitStatus {\n pub branch: Option<String>,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct ModelInfo {\n pub id: String,\n pub display_name: String,\n pub provider: String,\n pub api_protocol: ApiProtocol,\n pub base_url: String,\n pub model_name: String,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct SoulRef {\n pub id: String,\n pub description: String,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct ConfigIssue {\n pub message: String,\n pub section: InitConfigSection,\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\npub enum InitConfigSection {\n Language,\n ModelPool,\n ModelAssignment,\n Soul,\n Capability,\n Workspace,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub enum InitMessage {\n Info(String),\n Success(String),\n Warning(String),\n Error(String),\n Running(String),\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\npub enum CheckItemStatus {\n Done,\n Active,\n Pending,\n Warning,\n Failed,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct InitCheckItem {\n pub index: usize,\n pub title: String,\n pub status: CheckItemStatus,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct InitErrorState {\n pub source_state: InitState,\n pub message: String,\n pub recover_options: Vec<RecoverAction>,\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\npub enum RecoverAction {\n Retry,\n Back,\n Skip,\n Cancel,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct InitContext {\n pub cwd: PathBuf,\n pub git: Option<GitStatus>,\n pub language: Option<String>,\n pub selected_provider: Option<String>,\n pub api_protocol: Option<ApiProtocol>,\n pub base_url: Option<String>,\n pub api_key: Option<String>,\n pub fetched_models: Vec<ModelInfo>,\n pub selected_model_indices: Vec<usize>,\n pub model_pool: Vec<ModelInfo>,\n pub plan_model: Option<String>,\n pub execute_model: Option<String>,\n pub review_model: Option<String>,\n pub soul_choices: Vec<SoulRef>,\n pub selected_soul: Option<String>,\n pub capability_lock_exists: bool,\n pub workspace_created: bool,\n pub issues: Vec<ConfigIssue>,\n pub message_log: Vec<InitMessage>,\n pub selected_index: usize,\n pub error: Option<InitErrorState>,\n}\n\nimpl InitContext {\n pub fn new(cwd: PathBuf) -> Self {\n Self {\n cwd,\n git: None,\n language: None,\n selected_provider: None,\n api_protocol: None,\n base_url: None,\n api_key: None,\n fetched_models: Vec::new(),\n selected_model_indices: Vec::new(),\n model_pool: Vec::new(),\n plan_model: None,\n execute_model: None,\n review_model: None,\n soul_choices: Vec::new(),\n selected_soul: None,\n capability_lock_exists: false,\n workspace_created: false,\n issues: Vec::new(),\n message_log: Vec::new(),\n selected_index: 0,\n error: None,\n }\n }\n\n pub fn with_existing_config(\n cwd: PathBuf,\n language: Option<String>,\n model_pool: Vec<ModelInfo>,\n plan_model: Option<String>,\n execute_model: Option<String>,\n review_model: Option<String>,\n selected_soul: Option<String>,\n soul_choices: Vec<SoulRef>,\n ) -> Self {\n let mut context = Self::new(cwd);\n context.language = language;\n context.model_pool = model_pool;\n context.plan_model = plan_model;\n context.execute_model = execute_model;\n context.review_model = review_model;\n context.selected_soul = selected_soul;\n context.soul_choices = soul_choices;\n context\n }\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct InitWizard {\n pub state: InitState,\n pub context: InitContext,\n}\n\nimpl InitWizard {\n pub fn new(cwd: PathBuf) -> Self {\n Self {\n state: InitState::Start,\n context: InitContext::new(cwd),\n }\n }\n\n pub fn with_context(context: InitContext) -> Self {\n Self {\n state: InitState::Start,\n context,\n }\n }\n\n pub fn resume(mut self) -> Self {\n self.state = InitState::Resume;\n self.context.selected_index = 0;\n self\n }\n\n pub fn handle_event(&mut self, event: InitEvent) -> InitCommand {\n match event {\n InitEvent::Select(index) => {\n self.context.selected_index = index.min(self.max_selected_index());\n InitCommand::None\n }\n InitEvent::Toggle(index) => {\n self.toggle_index(index);\n InitCommand::None\n }\n InitEvent::Back => self.back(),\n InitEvent::Cancel => self.cancel(),\n InitEvent::Confirm => self.confirm(),\n InitEvent::TextInput(value) => self.text_input(value),\n InitEvent::WorkspaceChecked(result) => self.workspace_checked(result),\n InitEvent::ProjectCreated => self.project_created(),\n InitEvent::ModelsFetched(models) => self.models_fetched(models),\n InitEvent::ModelsImported(ids) => self.models_imported(ids),\n InitEvent::CapabilityResolved => self.capability_resolved(),\n InitEvent::WorkspaceCreated => self.workspace_created(),\n InitEvent::ValidationPassed => self.validation_passed(),\n InitEvent::ValidationFailed(issues) => self.validation_failed(issues),\n InitEvent::AsyncOk => self.async_ok(),\n InitEvent::AsyncFailed(message) => self.async_failed(message),\n InitEvent::Start => {\n self.state = InitState::Start;\n self.context.selected_index = 0;\n InitCommand::None\n }\n }\n }\n\n pub fn view_model(&self) -> InitViewModel {\n InitViewModel {\n header: self.header_text(),\n messages: self.render_messages(),\n check_items: self.render_check_items(),\n action_panel: self.action_panel(),\n footer: self.footer_text(),\n scope_title: self.scope_title().to_string(),\n }\n }\n\n fn confirm(&mut self) -> InitCommand {\n match self.state {\n InitState::Start => match self.selected_value().as_deref() {\n Some(\"exit\") => self.cancel(),\n _ => {\n self.state = InitState::CheckWorkspace;\n self.context.selected_index = 0;\n self.context\n .message_log\n .push(InitMessage::Running(\"Checking workspace.\".to_string()));\n InitCommand::CheckWorkspace\n }\n },\n InitState::Resume => match self.selected_value().as_deref() {\n Some(\"restart\") => {\n self.state = InitState::Start;\n self.context.message_log.clear();\n self.context.selected_index = 0;\n InitCommand::None\n }\n Some(\"exit\") => self.cancel(),\n _ => {\n self.state = self.next_unfinished_state();\n self.context.selected_index = 0;\n InitCommand::None\n }\n },\n InitState::CreateProject => match self.selected_value().as_deref() {\n Some(\"reinitialize_project\") => InitCommand::CreateProjectDirs { reset: true },\n Some(\"exit\") => self.cancel(),\n _ => self.cancel(),\n },\n InitState::SelectLanguage => match self.selected_value().as_deref() {\n Some(\"back\") => self.back(),\n Some(\"exit\") => self.cancel(),\n Some(value) => {\n let locale = if value == \"system\" {\n \"system\".to_string()\n } else {\n value.to_string()\n };\n self.context.language = Some(locale.clone());\n self.context\n .message_log\n .push(InitMessage::Success(format!(\"Language selected: {locale}\")));\n self.state = InitState::ConfigureModelPool;\n self.context.selected_index = 0;\n InitCommand::WriteLanguageConfig { locale }\n }\n None => InitCommand::None,\n },\n InitState::ConfigureModelPool => match self.selected_value().as_deref() {\n Some(\"add_model\") => {\n self.state = InitState::ModelSelectProvider;\n self.context.selected_index = 0;\n InitCommand::None\n }\n Some(\"configure_later\") | Some(\"continue_assignment\") => {\n self.state = InitState::ConfigureModelAssignment;\n self.context.selected_index = 0;\n InitCommand::None\n }\n Some(\"back\") => self.back(),\n Some(\"exit\") => self.cancel(),\n _ => InitCommand::None,\n },\n InitState::ModelSelectProvider => match self.selected_model_provider_choice() {\n Some(choice) => {\n self.context.selected_provider = Some(choice.provider.to_string());\n self.context.api_protocol = Some(choice.api_protocol);\n self.context.base_url = Some(choice.base_url.to_string());\n self.state = InitState::ModelInputBaseUrl;\n self.context.selected_index = 0;\n InitCommand::None\n }\n None => InitCommand::None,\n },\n InitState::ModelImportSelect => {\n let selected = self.selected_models_for_import();\n if selected.is_empty() {\n self.enter_error(\n InitState::ModelImportSelect,\n \"Select at least one model to import.\",\n vec![RecoverAction::Back, RecoverAction::Cancel],\n );\n InitCommand::None\n } else {\n InitCommand::ImportModels(selected)\n }\n }\n InitState::ConfigureModelAssignment => match self.selected_value().as_deref() {\n Some(\"plan\") => self.enter_assignment_state(InitState::SelectPlanModel),\n Some(\"execute\") => self.enter_assignment_state(InitState::SelectExecuteModel),\n Some(\"review\") => self.enter_assignment_state(InitState::SelectReviewModel),\n Some(\"continue_soul\") => {\n if let Some(command) = self.assignment_command() {\n self.state = InitState::ConfigureSoul;\n self.context.selected_index = 0;\n command\n } else {\n self.enter_error(\n InitState::ConfigureModelAssignment,\n \"Plan, Execute, and Review models must be assigned.\",\n vec![RecoverAction::Back, RecoverAction::Cancel],\n );\n InitCommand::None\n }\n }\n Some(\"back\") => self.back(),\n Some(\"exit\") => self.cancel(),\n _ => InitCommand::None,\n },\n InitState::SelectPlanModel\n | InitState::SelectExecuteModel\n | InitState::SelectReviewModel => match self.selected_value().as_deref() {\n Some(\"back\") => self.back(),\n Some(model_id) => {\n match self.state {\n InitState::SelectPlanModel => {\n self.context.plan_model = Some(model_id.to_string())\n }\n InitState::SelectExecuteModel => {\n self.context.execute_model = Some(model_id.to_string())\n }\n InitState::SelectReviewModel => {\n self.context.review_model = Some(model_id.to_string())\n }\n _ => {}\n }\n self.state = InitState::ConfigureModelAssignment;\n self.context.selected_index = 0;\n InitCommand::None\n }\n None => InitCommand::None,\n },\n InitState::ConfigureSoul => match self.selected_value().as_deref() {\n Some(\"back\") => self.back(),\n Some(\"exit\") => self.cancel(),\n Some(soul) => {\n self.context.selected_soul = Some(soul.to_string());\n self.context.selected_index = 0;\n InitCommand::WriteSoulConfig(soul.to_string())\n }\n None => InitCommand::None,\n },\n InitState::ResolveCapability => match self.selected_value().as_deref() {\n Some(\"skip_capability\") => {\n self.state = InitState::Complete;\n self.context.selected_index = 0;\n InitCommand::Complete\n }\n Some(\"back\") => self.back(),\n Some(\"exit\") => self.cancel(),\n _ => {\n self.state = InitState::Complete;\n self.context.selected_index = 0;\n InitCommand::Complete\n }\n },\n InitState::CreateWorkspace => match self.selected_value().as_deref() {\n Some(\"back\") => self.back(),\n Some(\"exit\") => self.cancel(),\n _ => InitCommand::CreateWorkspaceFromTemplate,\n },\n InitState::Validate => match self.selected_value().as_deref() {\n Some(\"fix_validation\") => {\n self.state = self.state_for_first_issue();\n self.context.issues.clear();\n self.context.selected_index = 0;\n InitCommand::None\n }\n Some(\"ignore_validation\") => {\n self.context.issues.clear();\n self.state = InitState::Complete;\n self.context.selected_index = 0;\n InitCommand::None\n }\n Some(\"back\") => self.back(),\n Some(\"exit\") => self.cancel(),\n _ => InitCommand::ValidateConfig,\n },\n InitState::Complete => match self.selected_value().as_deref() {\n Some(\"view_config\") => {\n self.context\n .message_log\n .push(InitMessage::Info(self.summary()));\n InitCommand::None\n }\n _ => InitCommand::Complete,\n },\n InitState::Error => self.recover(),\n InitState::CheckWorkspace\n | InitState::ModelInputBaseUrl\n | InitState::ModelInputApiKey\n | InitState::ModelFetchList\n | InitState::Cancelled => InitCommand::None,\n }\n }\n\n fn text_input(&mut self, value: String) -> InitCommand {\n match self.state {\n InitState::ModelInputBaseUrl => {\n let url = value.trim();\n if !valid_url(url) {\n self.enter_error(\n InitState::ModelInputBaseUrl,\n \"Base URL format error: missing protocol. Example: https://api.example.com/v1\",\n vec![RecoverAction::Retry, RecoverAction::Back, RecoverAction::Cancel],\n );\n InitCommand::None\n } else {\n self.context.base_url = Some(url.to_string());\n self.state = InitState::ModelInputApiKey;\n self.context.selected_index = 0;\n InitCommand::None\n }\n }\n InitState::ModelInputApiKey => {\n let api_key = value.trim().to_string();\n self.context.api_key = Some(api_key.clone());\n self.state = InitState::ModelFetchList;\n self.context\n .message_log\n .push(InitMessage::Running(\"Fetching model list.\".to_string()));\n InitCommand::FetchModels {\n provider: self.context.selected_provider.clone().unwrap_or_default(),\n api_protocol: self.context.api_protocol.unwrap_or(ApiProtocol::OpenAi),\n base_url: self.context.base_url.clone().unwrap_or_default(),\n api_key,\n }\n }\n _ => InitCommand::None,\n }\n }\n\n fn workspace_checked(&mut self, result: WorkspaceCheckResult) -> InitCommand {\n if result.ok {\n self.context.git = result.git;\n self.context.message_log.push(InitMessage::Success(\n result\n .message\n .unwrap_or_else(|| \"Workspace check complete.\".to_string()),\n ));\n // Fresh workspace: auto-create .alius without prompting.\n // Existing .alius: land on CreateProject so the user picks Reinitialize/Exit.\n if !result.alius_existed {\n self.state = InitState::CreateProject;\n self.context.selected_index = 0;\n return InitCommand::CreateProjectDirs { reset: false };\n }\n self.state = InitState::CreateProject;\n self.context.selected_index = 0;\n } else {\n self.enter_error(\n InitState::CheckWorkspace,\n result\n .message\n .unwrap_or_else(|| \"Workspace check failed.\".to_string()),\n vec![\n RecoverAction::Retry,\n RecoverAction::Back,\n RecoverAction::Cancel,\n ],\n );\n }\n InitCommand::None\n }\n\n fn project_created(&mut self) -> InitCommand {\n self.context.message_log.push(InitMessage::Success(\n \".alius project structure ready.\".to_string(),\n ));\n self.state = InitState::SelectLanguage;\n self.context.selected_index = 0;\n InitCommand::None\n }\n\n fn models_fetched(&mut self, models: Vec<ModelInfo>) -> InitCommand {\n if models.is_empty() {\n self.enter_error(\n InitState::ModelInputApiKey,\n \"No models returned. Check API Key or Base URL and try again.\",\n vec![\n RecoverAction::Retry,\n RecoverAction::Back,\n RecoverAction::Cancel,\n ],\n );\n return InitCommand::None;\n }\n let model_count = models.len();\n self.context.fetched_models = models;\n self.context.selected_model_indices = (0..self.context.fetched_models.len()).collect();\n self.context.message_log.push(InitMessage::Success(format!(\n \"Fetched {model_count} model(s).\"\n )));\n self.state = InitState::ModelImportSelect;\n self.context.selected_index = 0;\n InitCommand::None\n }\n\n fn models_imported(&mut self, ids: Vec<String>) -> InitCommand {\n for model in self\n .context\n .fetched_models\n .iter()\n .filter(|model| ids.iter().any(|id| id == &model.id))\n {\n if let Some(existing) = self\n .context\n .model_pool\n .iter_mut()\n .find(|entry| entry.id == model.id)\n {\n *existing = model.clone();\n } else {\n self.context.model_pool.push(model.clone());\n }\n }\n self.context.message_log.push(InitMessage::Success(format!(\n \"Imported {} model(s).\",\n ids.len()\n )));\n self.state = InitState::ConfigureModelAssignment;\n self.context.selected_index = 0;\n InitCommand::None\n }\n\n fn capability_resolved(&mut self) -> InitCommand {\n self.context.capability_lock_exists = true;\n self.context.message_log.push(InitMessage::Success(\n \"Capability lock generated.\".to_string(),\n ));\n self.state = InitState::Complete;\n self.context.selected_index = 0;\n InitCommand::None\n }\n\n fn workspace_created(&mut self) -> InitCommand {\n self.context.workspace_created = true;\n self.context.message_log.push(InitMessage::Success(\n \"Workspace template created.\".to_string(),\n ));\n self.state = InitState::Validate;\n self.context.selected_index = 0;\n InitCommand::None\n }\n\n fn validation_passed(&mut self) -> InitCommand {\n self.context.issues.clear();\n self.context\n .message_log\n .push(InitMessage::Success(\"Final validation passed.\".to_string()));\n self.state = InitState::Complete;\n self.context.selected_index = 0;\n InitCommand::None\n }\n\n fn validation_failed(&mut self, issues: Vec<ConfigIssue>) -> InitCommand {\n self.context.issues = issues;\n self.enter_error(\n InitState::Validate,\n \"Final validation found unfinished items.\",\n vec![\n RecoverAction::Retry,\n RecoverAction::Back,\n RecoverAction::Skip,\n RecoverAction::Cancel,\n ],\n );\n InitCommand::None\n }\n\n fn async_ok(&mut self) -> InitCommand {\n match self.state {\n InitState::CreateProject => self.project_created(),\n InitState::ConfigureSoul => {\n let soul = self\n .context\n .selected_soul\n .clone()\n .unwrap_or_else(|| \"Role\".to_string());\n self.context\n .message_log\n .push(InitMessage::Success(format!(\"Role selected: {soul}\")));\n self.state = InitState::Complete;\n self.context.selected_index = 0;\n InitCommand::Complete\n }\n InitState::ResolveCapability => self.capability_resolved(),\n InitState::CreateWorkspace => self.workspace_created(),\n _ => InitCommand::None,\n }\n }\n\n fn async_failed(&mut self, message: String) -> InitCommand {\n let source_state = match self.state {\n InitState::ModelFetchList => InitState::ModelInputApiKey,\n other => other,\n };\n self.enter_error(\n source_state,\n message,\n vec![\n RecoverAction::Retry,\n RecoverAction::Back,\n RecoverAction::Skip,\n RecoverAction::Cancel,\n ],\n );\n InitCommand::None\n }\n\n pub fn back(&mut self) -> InitCommand {\n self.state = match self.state {\n InitState::Resume => InitState::Start,\n InitState::CreateProject => InitState::Start,\n InitState::SelectLanguage => InitState::CreateProject,\n InitState::ConfigureModelPool => InitState::SelectLanguage,\n InitState::ModelSelectProvider => InitState::ConfigureModelPool,\n InitState::ModelInputBaseUrl => InitState::ModelSelectProvider,\n InitState::ModelInputApiKey => InitState::ModelInputBaseUrl,\n InitState::ModelImportSelect => InitState::ModelInputApiKey,\n InitState::ConfigureModelAssignment => InitState::ConfigureModelPool,\n InitState::SelectPlanModel\n | InitState::SelectExecuteModel\n | InitState::SelectReviewModel => InitState::ConfigureModelAssignment,\n InitState::ConfigureSoul => InitState::ConfigureModelAssignment,\n InitState::ResolveCapability => InitState::ConfigureSoul,\n InitState::CreateWorkspace | InitState::Validate => InitState::ConfigureSoul,\n InitState::Error => self\n .context\n .error\n .as_ref()\n .map(|error| previous_state(error.source_state))\n .unwrap_or(InitState::Start),\n InitState::Complete => InitState::ConfigureSoul,\n state => state,\n };\n self.context.error = None;\n self.context.selected_index = 0;\n InitCommand::None\n }\n\n fn cancel(&mut self) -> InitCommand {\n self.state = InitState::Cancelled;\n self.context.selected_index = 0;\n InitCommand::Cancel\n }\n\n fn recover(&mut self) -> InitCommand {\n let Some(error) = self.context.error.clone() else {\n self.state = InitState::Start;\n return InitCommand::None;\n };\n match self.selected_recover_action(&error) {\n Some(RecoverAction::Retry) => {\n self.state = error.source_state;\n self.context.error = None;\n self.context.selected_index = 0;\n match self.state {\n InitState::CheckWorkspace => InitCommand::CheckWorkspace,\n InitState::CreateProject => InitCommand::CreateProjectDirs { reset: false },\n InitState::ModelInputApiKey => InitCommand::None,\n InitState::ResolveCapability => InitCommand::Complete,\n InitState::CreateWorkspace => InitCommand::CreateWorkspaceFromTemplate,\n InitState::Validate => InitCommand::ValidateConfig,\n _ => InitCommand::None,\n }\n }\n Some(RecoverAction::Back) => self.back(),\n Some(RecoverAction::Skip) => {\n self.context.error = None;\n self.context.selected_index = 0;\n self.state = match error.source_state {\n InitState::ResolveCapability => InitState::Complete,\n InitState::Validate => InitState::Complete,\n _ => previous_state(error.source_state),\n };\n if self.state == InitState::Complete {\n InitCommand::Complete\n } else {\n InitCommand::None\n }\n }\n Some(RecoverAction::Cancel) | None => self.cancel(),\n }\n }\n\n fn enter_assignment_state(&mut self, state: InitState) -> InitCommand {\n self.state = state;\n self.context.selected_index = 0;\n InitCommand::None\n }\n\n fn enter_error(\n &mut self,\n source_state: InitState,\n message: impl Into<String>,\n recover_options: Vec<RecoverAction>,\n ) {\n let message = message.into();\n self.context\n .message_log\n .push(InitMessage::Error(message.clone()));\n self.context.error = Some(InitErrorState {\n source_state,\n message,\n recover_options,\n });\n self.state = InitState::Error;\n self.context.selected_index = 0;\n }\n\n fn toggle_index(&mut self, index: usize) {\n if self.state != InitState::ModelImportSelect {\n return;\n }\n if index >= self.context.fetched_models.len() {\n return;\n }\n if let Some(pos) = self\n .context\n .selected_model_indices\n .iter()\n .position(|selected| *selected == index)\n {\n self.context.selected_model_indices.remove(pos);\n } else {\n self.context.selected_model_indices.push(index);\n self.context.selected_model_indices.sort_unstable();\n }\n }\n\n fn assignment_command(&self) -> Option<InitCommand> {\n let plan = self.context.plan_model.clone()?;\n let execute = self.context.execute_model.clone()?;\n let review = self.context.review_model.clone()?;\n Some(InitCommand::WriteModelAssignment {\n plan,\n execute,\n review,\n })\n }\n\n fn state_for_first_issue(&self) -> InitState {\n self.context\n .issues\n .first()\n .map(|issue| match issue.section {\n InitConfigSection::Language => InitState::SelectLanguage,\n InitConfigSection::ModelPool => InitState::ConfigureModelPool,\n InitConfigSection::ModelAssignment => InitState::ConfigureModelAssignment,\n InitConfigSection::Soul => InitState::ConfigureSoul,\n InitConfigSection::Capability | InitConfigSection::Workspace => InitState::Complete,\n })\n .unwrap_or(InitState::Validate)\n }\n\n fn next_unfinished_state(&self) -> InitState {\n if self.context.language.is_none() {\n InitState::SelectLanguage\n } else if self.context.model_pool.is_empty() {\n InitState::ConfigureModelPool\n } else if self.assignment_command().is_none() {\n InitState::ConfigureModelAssignment\n } else if self.context.selected_soul.is_none() {\n InitState::ConfigureSoul\n } else {\n InitState::Complete\n }\n }\n\n fn selected_value(&self) -> Option<String> {\n match self.action_panel() {\n ActionPanel::SingleChoice {\n options, selected, ..\n }\n | ActionPanel::Summary {\n options, selected, ..\n } => options.get(selected).map(|option| option.value.clone()),\n _ => None,\n }\n }\n\n fn selected_recover_action(&self, error: &InitErrorState) -> Option<RecoverAction> {\n error\n .recover_options\n .get(self.context.selected_index)\n .copied()\n }\n\n fn selected_model_provider_choice(&self) -> Option<ModelProviderChoice> {\n model_provider_choices()\n .get(self.context.selected_index)\n .copied()\n }\n\n fn selected_models_for_import(&self) -> Vec<ModelInfo> {\n self.context\n .selected_model_indices\n .iter()\n .filter_map(|index| self.context.fetched_models.get(*index).cloned())\n .collect()\n }\n\n fn max_selected_index(&self) -> usize {\n match self.action_panel() {\n ActionPanel::SingleChoice { options, .. } | ActionPanel::Summary { options, .. } => {\n options.len().saturating_sub(1)\n }\n ActionPanel::MultiChoice { options, .. } => options.len().saturating_sub(1),\n _ => 0,\n }\n }\n\n fn header_text(&self) -> String {\n match self.state {\n InitState::Complete => \"Alius ✓ configuration complete\".to_string(),\n _ => \"Alius ✕ configuration incomplete\".to_string(),\n }\n }\n\n fn footer_text(&self) -> String {\n match &self.context.git {\n Some(git) => match &git.branch {\n Some(branch) => format!(\"cwd: {} git: {}\", self.context.cwd.display(), branch),\n None => format!(\"cwd: {} git\", self.context.cwd.display()),\n },\n None => format!(\"cwd: {}\", self.context.cwd.display()),\n }\n }\n\n fn render_messages(&self) -> Vec<RenderedMessage> {\n let mut messages = self\n .context\n .message_log\n .iter()\n .map(|message| match message {\n InitMessage::Info(text) => RenderedMessage::new(\"●\", text),\n InitMessage::Success(text) => RenderedMessage::new(\"✓\", text),\n InitMessage::Warning(text) => RenderedMessage::new(\"!\", text),\n InitMessage::Error(text) => RenderedMessage::new(\"!\", text),\n InitMessage::Running(text) => RenderedMessage::new(\"○\", text),\n })\n .collect::<Vec<_>>();\n if messages.is_empty() {\n messages.push(RenderedMessage::new(\n \"●\",\n \"Welcome to Alius. This workspace is not fully initialized yet.\",\n ));\n }\n messages\n }\n\n fn render_check_items(&self) -> Vec<RenderedCheckItem> {\n let active = self.checklist_state();\n INIT_CHECK_ITEMS\n .iter()\n .enumerate()\n .map(|(index, (state, title))| {\n let status = if self.state == InitState::Complete {\n CheckItemStatus::Done\n } else if *state == active {\n CheckItemStatus::Active\n } else if checklist_index(*state) < checklist_index(active) {\n CheckItemStatus::Done\n } else {\n CheckItemStatus::Pending\n };\n RenderedCheckItem {\n index: index + 1,\n title: (*title).to_string(),\n status,\n }\n })\n .collect()\n }\n\n fn action_panel(&self) -> ActionPanel {\n match self.state {\n InitState::Start => single(\n \"Start Initialization\",\n vec![\n option(\"Start Initialization\", \"start\"),\n option(\"Exit\", \"exit\"),\n ],\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n ),\n InitState::Resume => single(\n \"Detected unfinished initialization\",\n vec![\n option(\"Continue Previous\", \"continue\"),\n option(\"Restart\", \"restart\"),\n option(\"Exit\", \"exit\"),\n ],\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n ),\n InitState::CheckWorkspace | InitState::ModelFetchList => ActionPanel::None,\n InitState::CreateProject => single(\n \"Project structure\",\n vec![\n option(\"Reinitialize .alius Defaults\", \"reinitialize_project\"),\n option(\"Exit\", \"exit\"),\n ],\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n ),\n InitState::SelectLanguage => single(\n \"Choose interface language\",\n vec![\n option(\"Chinese (Simplified) - zh-CN\", \"zh-CN\"),\n option(\"English - en\", \"en\"),\n option(\"Japanese - ja\", \"ja\"),\n option(\"Follow System\", \"system\"),\n option(\"Back\", \"back\"),\n option(\"Exit\", \"exit\"),\n ],\n self.context.selected_index,\n \"Up/Down choose. Enter confirms. Esc returns.\",\n ),\n InitState::ConfigureModelPool => {\n let mut options = if self.context.model_pool.is_empty() {\n vec![\n option(\"Add Model\", \"add_model\"),\n option(\"Configure Later\", \"configure_later\"),\n ]\n } else {\n vec![\n option(\"Continue to Model Assignment\", \"continue_assignment\"),\n option(\"Add Another Model\", \"add_model\"),\n ]\n };\n options.push(option(\"Back\", \"back\"));\n options.push(option(\"Exit\", \"exit\"));\n single(\n \"Model pool\",\n options,\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n )\n }\n InitState::ModelSelectProvider => single(\n \"Choose model provider and API\",\n model_provider_choices()\n .iter()\n .map(|choice| option(choice.label, choice.value))\n .collect(),\n self.context.selected_index,\n \"Up/Down choose. Enter confirms. Esc returns.\",\n ),\n InitState::ModelInputBaseUrl => ActionPanel::TextInput {\n title: \"Enter Base URL\".to_string(),\n value: self.context.base_url.clone().unwrap_or_default(),\n placeholder: \"https://api.example.com/v1\".to_string(),\n hint: \"Enter confirms. Esc returns.\".to_string(),\n masked: false,\n },\n InitState::ModelInputApiKey => ActionPanel::TextInput {\n title: \"Enter API Key\".to_string(),\n value: String::new(),\n placeholder: \"API Key\".to_string(),\n hint: \"Enter confirms. Esc returns.\".to_string(),\n masked: false,\n },\n InitState::ModelImportSelect => ActionPanel::MultiChoice {\n title: format!(\"Found {} model(s)\", self.context.fetched_models.len()),\n options: self\n .context\n .fetched_models\n .iter()\n .enumerate()\n .map(|(index, model)| MultiChoiceOption {\n label: model.display_name.clone(),\n value: model.id.clone(),\n selected: self.context.selected_model_indices.contains(&index),\n })\n .collect(),\n highlighted: self.context.selected_index,\n hint: \"Space toggles. Enter imports selected. Esc returns.\".to_string(),\n },\n InitState::ConfigureModelAssignment => {\n let mut options = vec![\n option(\n &format!(\n \"Plan Model {}\",\n self.model_label(self.context.plan_model.as_deref())\n ),\n \"plan\",\n ),\n option(\n &format!(\n \"Execute Model {}\",\n self.model_label(self.context.execute_model.as_deref())\n ),\n \"execute\",\n ),\n option(\n &format!(\n \"Review Model {}\",\n self.model_label(self.context.review_model.as_deref())\n ),\n \"review\",\n ),\n ];\n if self.assignment_command().is_some() {\n options.push(option(\"Continue to Role\", \"continue_soul\"));\n }\n options.push(option(\"Back\", \"back\"));\n options.push(option(\"Exit\", \"exit\"));\n single(\n \"Plan / Execute / Review\",\n options,\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n )\n }\n InitState::SelectPlanModel\n | InitState::SelectExecuteModel\n | InitState::SelectReviewModel => {\n let mut options = self\n .context\n .model_pool\n .iter()\n .map(|model| option(&model.display_name, &model.id))\n .collect::<Vec<_>>();\n options.push(option(\"Back\", \"back\"));\n single(\n self.assignment_title(),\n options,\n self.context.selected_index,\n \"Up/Down choose. Enter assigns.\",\n )\n }\n InitState::ConfigureSoul => {\n let mut options = self\n .context\n .soul_choices\n .iter()\n .map(|soul| option(&format!(\"{} - {}\", soul.id, soul.description), &soul.id))\n .collect::<Vec<_>>();\n options.push(option(\"Back\", \"back\"));\n options.push(option(\"Exit\", \"exit\"));\n single(\n \"Choose Role\",\n options,\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n )\n }\n InitState::ResolveCapability => ActionPanel::None,\n InitState::CreateWorkspace => single(\n \"Create workspace\",\n vec![\n option(\"Create Workspace\", \"create_workspace\"),\n option(\"Back\", \"back\"),\n option(\"Exit\", \"exit\"),\n ],\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n ),\n InitState::Validate => {\n let options = if self.context.issues.is_empty() {\n vec![\n option(\"Run Final Validation\", \"run_validate\"),\n option(\"Back\", \"back\"),\n option(\"Exit\", \"exit\"),\n ]\n } else {\n vec![\n option(\"Return to Fix\", \"fix_validation\"),\n option(\"Ignore and Complete\", \"ignore_validation\"),\n option(\"Exit\", \"exit\"),\n ]\n };\n single(\n \"Final validation\",\n options,\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n )\n }\n InitState::Complete => ActionPanel::Summary {\n title: \"Initialization complete\".to_string(),\n lines: self.summary().lines().map(str::to_string).collect(),\n options: Vec::new(),\n selected: self.context.selected_index,\n hint: \"Working.\".to_string(),\n },\n InitState::Error => {\n let options = self\n .context\n .error\n .as_ref()\n .map(|error| {\n error\n .recover_options\n .iter()\n .map(|action| recover_option(*action))\n .collect()\n })\n .unwrap_or_else(|| vec![option(\"Exit\", \"cancel\")]);\n single(\n \"Recover\",\n options,\n self.context.selected_index,\n \"Up/Down choose. Enter confirms.\",\n )\n }\n InitState::Cancelled => single(\n \"Cancelled\",\n vec![option(\"Exit\", \"exit\")],\n self.context.selected_index,\n \"Enter confirms.\",\n ),\n }\n }\n\n /// The configurable stage the user is currently in (for the nav bar highlight).\n /// Returns `None` for automatic/pre/post stages.\n pub fn current_stage(&self) -> Option<InitStage> {\n match self.state {\n InitState::SelectLanguage => Some(InitStage::Language),\n InitState::ConfigureModelPool\n | InitState::ModelSelectProvider\n | InitState::ModelInputBaseUrl\n | InitState::ModelInputApiKey\n | InitState::ModelFetchList\n | InitState::ModelImportSelect => Some(InitStage::ModelPool),\n InitState::ConfigureModelAssignment\n | InitState::SelectPlanModel\n | InitState::SelectExecuteModel\n | InitState::SelectReviewModel => Some(InitStage::Assignment),\n InitState::ConfigureSoul => Some(InitStage::Soul),\n _ => None,\n }\n }\n\n /// Whether a stage's data is already set (for the nav bar ✓ marker).\n pub fn stage_done(&self, stage: InitStage) -> bool {\n match stage {\n InitStage::Language => self.context.language.is_some(),\n InitStage::ModelPool => !self.context.model_pool.is_empty(),\n InitStage::Assignment => {\n self.context.plan_model.is_some()\n && self.context.execute_model.is_some()\n && self.context.review_model.is_some()\n }\n InitStage::Soul => self.context.selected_soul.is_some(),\n }\n }\n\n fn checklist_state(&self) -> InitState {\n match self.state {\n InitState::Start | InitState::Resume | InitState::CheckWorkspace => {\n InitState::CheckWorkspace\n }\n InitState::CreateProject => InitState::CreateProject,\n InitState::SelectLanguage => InitState::SelectLanguage,\n InitState::ConfigureModelPool\n | InitState::ModelSelectProvider\n | InitState::ModelInputBaseUrl\n | InitState::ModelInputApiKey\n | InitState::ModelFetchList\n | InitState::ModelImportSelect => InitState::ConfigureModelPool,\n InitState::ConfigureModelAssignment\n | InitState::SelectPlanModel\n | InitState::SelectExecuteModel\n | InitState::SelectReviewModel => InitState::ConfigureModelAssignment,\n InitState::ConfigureSoul\n | InitState::ResolveCapability\n | InitState::CreateWorkspace\n | InitState::Validate\n | InitState::Complete => InitState::ConfigureSoul,\n InitState::Error => self\n .context\n .error\n .as_ref()\n .map(|error| error.source_state)\n .unwrap_or(InitState::CheckWorkspace),\n InitState::Cancelled => InitState::CheckWorkspace,\n }\n }\n\n fn scope_title(&self) -> &'static str {\n match self.state {\n InitState::Start => \"init-start\",\n InitState::Resume => \"resume\",\n InitState::CheckWorkspace => \"check-workspace\",\n InitState::CreateProject => \"create-project\",\n InitState::SelectLanguage => \"select-language\",\n InitState::ConfigureModelPool\n | InitState::ModelSelectProvider\n | InitState::ModelInputBaseUrl\n | InitState::ModelInputApiKey\n | InitState::ModelFetchList\n | InitState::ModelImportSelect => \"configure-model-pool\",\n InitState::ConfigureModelAssignment\n | InitState::SelectPlanModel\n | InitState::SelectExecuteModel\n | InitState::SelectReviewModel => \"configure-assignment\",\n InitState::ConfigureSoul => \"configure-soul\",\n InitState::ResolveCapability => \"complete\",\n InitState::CreateWorkspace => \"create-workspace\",\n InitState::Validate => \"validate\",\n InitState::Complete => \"complete\",\n InitState::Error => \"error\",\n InitState::Cancelled => \"cancelled\",\n }\n }\n\n fn assignment_title(&self) -> &'static str {\n match self.state {\n InitState::SelectPlanModel => \"Plan Model\",\n InitState::SelectExecuteModel => \"Execute Model\",\n InitState::SelectReviewModel => \"Review Model\",\n _ => \"Model\",\n }\n }\n\n fn model_label(&self, model_id: Option<&str>) -> String {\n let Some(id) = model_id.filter(|id| !id.trim().is_empty()) else {\n return \"not configured\".to_string();\n };\n self.context\n .model_pool\n .iter()\n .find(|model| model.id == id)\n .map(|model| model.display_name.clone())\n .unwrap_or_else(|| id.to_string())\n }\n\n fn summary(&self) -> String {\n format!(\n \"Role: {}\\nPlan: {}\\nExecute: {}\\nReview: {}\",\n self.context\n .selected_soul\n .clone()\n .unwrap_or_else(|| \"not configured\".to_string()),\n self.model_label(self.context.plan_model.as_deref()),\n self.model_label(self.context.execute_model.as_deref()),\n self.model_label(self.context.review_model.as_deref()),\n )\n }\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct InitViewModel {\n pub header: String,\n pub messages: Vec<RenderedMessage>,\n pub check_items: Vec<RenderedCheckItem>,\n pub action_panel: ActionPanel,\n pub footer: String,\n pub scope_title: String,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct RenderedMessage {\n pub marker: String,\n pub text: String,\n}\n\nimpl RenderedMessage {\n fn new(marker: &str, text: impl Into<String>) -> Self {\n Self {\n marker: marker.to_string(),\n text: text.into(),\n }\n }\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct RenderedCheckItem {\n pub index: usize,\n pub title: String,\n pub status: CheckItemStatus,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub enum ActionPanel {\n SingleChoice {\n title: String,\n options: Vec<ActionOption>,\n selected: usize,\n hint: String,\n },\n MultiChoice {\n title: String,\n options: Vec<MultiChoiceOption>,\n highlighted: usize,\n hint: String,\n },\n TextInput {\n title: String,\n value: String,\n placeholder: String,\n hint: String,\n masked: bool,\n },\n Summary {\n title: String,\n lines: Vec<String>,\n options: Vec<ActionOption>,\n selected: usize,\n hint: String,\n },\n None,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct ActionOption {\n pub label: String,\n pub value: String,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct MultiChoiceOption {\n pub label: String,\n pub value: String,\n pub selected: bool,\n}\n\nconst INIT_CHECK_ITEMS: &[(InitState, &str)] = &[\n (InitState::CheckWorkspace, \"Check workspace\"),\n (InitState::CreateProject, \"Initialize .alius/\"),\n (InitState::SelectLanguage, \"Choose language\"),\n (InitState::ConfigureModelPool, \"Configure model pool\"),\n (\n InitState::ConfigureModelAssignment,\n \"Configure Plan/Execute/Review\",\n ),\n (InitState::ConfigureSoul, \"Configure Role\"),\n];\n\n#[derive(Debug, Clone, Copy)]\nstruct ModelProviderChoice {\n label: &'static str,\n value: &'static str,\n provider: &'static str,\n api_protocol: ApiProtocol,\n base_url: &'static str,\n}\n\nfn model_provider_choices() -> &'static [ModelProviderChoice] {\n &[\n ModelProviderChoice {\n label: \"BigModel GLM (Coding Plan) - OpenAI API\",\n value: \"bigmodel-openai\",\n provider: \"bigmodel\",\n api_protocol: ApiProtocol::OpenAi,\n base_url: \"https://open.bigmodel.cn/api/coding/paas/v4\",\n },\n ModelProviderChoice {\n label: \"BigModel GLM (Coding Plan) - Anthropic API\",\n value: \"bigmodel-anthropic\",\n provider: \"bigmodel\",\n api_protocol: ApiProtocol::Anthropic,\n base_url: \"https://open.bigmodel.cn/api/anthropic\",\n },\n ModelProviderChoice {\n label: \"Xiaomi MiMo (Token Plan) - OpenAI API\",\n value: \"xiaomi-mimo-openai\",\n provider: \"xiaomi_mimo\",\n api_protocol: ApiProtocol::OpenAi,\n base_url: \"https://api.xiaomimimo.com/v1\",\n },\n ModelProviderChoice {\n label: \"Xiaomi MiMo (Token Plan) - Anthropic API\",\n value: \"xiaomi-mimo-anthropic\",\n provider: \"xiaomi_mimo\",\n api_protocol: ApiProtocol::Anthropic,\n base_url: \"https://api.xiaomimimo.com/anthropic\",\n },\n ModelProviderChoice {\n label: \"DeepSeek - OpenAI API\",\n value: \"deepseek-openai\",\n provider: \"deepseek\",\n api_protocol: ApiProtocol::OpenAi,\n base_url: \"https://api.deepseek.com\",\n },\n ModelProviderChoice {\n label: \"DeepSeek - Anthropic API\",\n value: \"deepseek-anthropic\",\n provider: \"deepseek\",\n api_protocol: ApiProtocol::Anthropic,\n base_url: \"https://api.deepseek.com/anthropic\",\n },\n ]\n}\n\nfn previous_state(state: InitState) -> InitState {\n match state {\n InitState::CreateProject => InitState::Start,\n InitState::SelectLanguage => InitState::CreateProject,\n InitState::ConfigureModelPool => InitState::SelectLanguage,\n InitState::ModelSelectProvider => InitState::ConfigureModelPool,\n InitState::ModelInputBaseUrl => InitState::ModelSelectProvider,\n InitState::ModelInputApiKey => InitState::ModelInputBaseUrl,\n InitState::ModelImportSelect => InitState::ModelInputApiKey,\n InitState::ConfigureModelAssignment => InitState::ConfigureModelPool,\n InitState::SelectPlanModel\n | InitState::SelectExecuteModel\n | InitState::SelectReviewModel => InitState::ConfigureModelAssignment,\n InitState::ConfigureSoul => InitState::ConfigureModelAssignment,\n InitState::ResolveCapability | InitState::CreateWorkspace | InitState::Validate => {\n InitState::ConfigureSoul\n }\n _ => InitState::Start,\n }\n}\n\nfn checklist_index(state: InitState) -> usize {\n INIT_CHECK_ITEMS\n .iter()\n .position(|(item_state, _)| *item_state == state)\n .unwrap_or(0)\n}\n\nfn valid_url(value: &str) -> bool {\n value.starts_with(\"https://\") || value.starts_with(\"http://\")\n}\n\nfn single(\n title: impl Into<String>,\n options: Vec<ActionOption>,\n selected: usize,\n hint: impl Into<String>,\n) -> ActionPanel {\n ActionPanel::SingleChoice {\n title: title.into(),\n selected: selected.min(options.len().saturating_sub(1)),\n options,\n hint: hint.into(),\n }\n}\n\nfn option(label: impl Into<String>, value: impl Into<String>) -> ActionOption {\n ActionOption {\n label: label.into(),\n value: value.into(),\n }\n}\n\nfn recover_option(action: RecoverAction) -> ActionOption {\n match action {\n RecoverAction::Retry => option(\"Retry\", \"retry\"),\n RecoverAction::Back => option(\"Back\", \"back\"),\n RecoverAction::Skip => option(\"Skip\", \"skip\"),\n RecoverAction::Cancel => option(\"Exit\", \"cancel\"),\n }\n}\n\npub fn model_info_from_library(entry: &ModelLibraryEntry) -> ModelInfo {\n ModelInfo {\n id: entry.id.clone(),\n display_name: entry.display_name.clone(),\n provider: entry.provider.clone(),\n api_protocol: if entry.base_url.contains(\"/anthropic\") {\n ApiProtocol::Anthropic\n } else {\n ApiProtocol::OpenAi\n },\n base_url: entry.base_url.clone(),\n model_name: entry.model_name.clone(),\n }\n}\n\npub fn role_from_assignment_state(state: InitState) -> Option<ModelAssignmentRole> {\n match state {\n InitState::SelectPlanModel => Some(ModelAssignmentRole::Plan),\n InitState::SelectExecuteModel => Some(ModelAssignmentRole::Execute),\n InitState::SelectReviewModel => Some(ModelAssignmentRole::Review),\n _ => None,\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n fn wizard() -> InitWizard {\n InitWizard::new(PathBuf::from(\"/tmp/project\"))\n }\n\n #[test]\n fn start_confirm_goes_to_check_workspace() {\n let mut wizard = wizard();\n\n let command = wizard.handle_event(InitEvent::Confirm);\n\n assert_eq!(wizard.state, InitState::CheckWorkspace);\n assert_eq!(command, InitCommand::CheckWorkspace);\n }\n\n #[test]\n fn workspace_ok_goes_to_create_project() {\n let mut wizard = wizard();\n wizard.state = InitState::CheckWorkspace;\n\n let command = wizard.handle_event(InitEvent::WorkspaceChecked(WorkspaceCheckResult {\n ok: true,\n git: Some(GitStatus {\n branch: Some(\"main\".to_string()),\n }),\n message: None,\n alius_existed: true,\n }));\n\n assert_eq!(wizard.state, InitState::CreateProject);\n assert_eq!(command, InitCommand::None);\n assert!(wizard.context.git.is_some());\n }\n\n #[test]\n fn workspace_failure_enters_error() {\n let mut wizard = wizard();\n wizard.state = InitState::CheckWorkspace;\n\n wizard.handle_event(InitEvent::WorkspaceChecked(WorkspaceCheckResult {\n ok: false,\n git: None,\n message: Some(\"no permission\".to_string()),\n alius_existed: true,\n }));\n\n assert_eq!(wizard.state, InitState::Error);\n assert_eq!(\n wizard\n .context\n .error\n .as_ref()\n .map(|error| error.source_state),\n Some(InitState::CheckWorkspace)\n );\n }\n\n #[test]\n fn select_language_returns_write_command() {\n let mut wizard = wizard();\n wizard.state = InitState::SelectLanguage;\n wizard.handle_event(InitEvent::Select(1));\n\n let command = wizard.handle_event(InitEvent::Confirm);\n\n assert_eq!(wizard.state, InitState::ConfigureModelPool);\n assert_eq!(\n command,\n InitCommand::WriteLanguageConfig {\n locale: \"en\".to_string()\n }\n );\n }\n\n #[test]\n fn model_fetch_failure_enters_api_key_error() {\n let mut wizard = wizard();\n wizard.state = InitState::ModelFetchList;\n\n wizard.handle_event(InitEvent::AsyncFailed(\"bad key\".to_string()));\n\n assert_eq!(wizard.state, InitState::Error);\n assert_eq!(\n wizard\n .context\n .error\n .as_ref()\n .map(|error| error.source_state),\n Some(InitState::ModelInputApiKey)\n );\n }\n\n #[test]\n fn models_fetched_go_to_import_select() {\n let mut wizard = wizard();\n wizard.state = InitState::ModelFetchList;\n\n wizard.handle_event(InitEvent::ModelsFetched(vec![ModelInfo {\n id: \"m1\".to_string(),\n display_name: \"m1\".to_string(),\n provider: \"bigmodel\".to_string(),\n api_protocol: ApiProtocol::OpenAi,\n base_url: \"https://example.com/v1\".to_string(),\n model_name: \"m1\".to_string(),\n }]));\n\n assert_eq!(wizard.state, InitState::ModelImportSelect);\n assert_eq!(wizard.context.selected_model_indices, vec![0]);\n }\n\n #[test]\n fn validation_passed_goes_complete() {\n let mut wizard = wizard();\n wizard.state = InitState::Validate;\n\n wizard.handle_event(InitEvent::ValidationPassed);\n\n assert_eq!(wizard.state, InitState::Complete);\n }\n\n #[test]\n fn validation_failed_enters_error() {\n let mut wizard = wizard();\n wizard.state = InitState::Validate;\n\n wizard.handle_event(InitEvent::ValidationFailed(vec![ConfigIssue {\n message: \"Review Model is not configured.\".to_string(),\n section: InitConfigSection::ModelAssignment,\n }]));\n\n assert_eq!(wizard.state, InitState::Error);\n assert_eq!(wizard.context.issues.len(), 1);\n }\n\n #[test]\n fn legacy_capability_state_completes_without_visible_step() {\n let mut wizard = wizard();\n wizard.state = InitState::ResolveCapability;\n\n let command = wizard.handle_event(InitEvent::Confirm);\n\n assert_eq!(wizard.state, InitState::Complete);\n assert_eq!(command, InitCommand::Complete);\n let titles = wizard\n .view_model()\n .check_items\n .into_iter()\n .map(|item| item.title)\n .collect::<Vec<_>>();\n assert!(!titles.iter().any(|title| title == \"Resolve Capability\"));\n assert!(!titles.iter().any(|title| title == \"Create workspace\"));\n assert!(!titles.iter().any(|title| title == \"Validate configuration\"));\n }\n\n #[test]\n fn complete_view_has_no_final_mode_choice() {\n let mut wizard = wizard();\n wizard.state = InitState::Complete;\n\n let vm = wizard.view_model();\n\n let ActionPanel::Summary { options, .. } = vm.action_panel else {\n panic!(\"expected complete summary panel\");\n };\n assert!(options.is_empty());\n }\n\n #[test]\n fn start_view_model_uses_single_choice() {\n let wizard = wizard();\n let vm = wizard.view_model();\n\n assert_eq!(vm.scope_title, \"init-start\");\n assert!(matches!(vm.action_panel, ActionPanel::SingleChoice { .. }));\n }\n\n #[test]\n fn api_key_view_model_is_plaintext() {\n let mut wizard = wizard();\n wizard.state = InitState::ModelInputApiKey;\n let vm = wizard.view_model();\n\n assert!(matches!(\n vm.action_panel,\n ActionPanel::TextInput { masked: false, .. }\n ));\n }\n}\n"}}, "entrypoints/cli/src/tui/config_panel.rs": {"3d56e304e096c6e5745d05444eeed4a5d591db55": {"t": 1785746234.5425675, "n": 33555, "h": "074947392193652c", "c": "use crate::formula::FormulaDef;\nuse anyhow::Result;\nuse crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};\nuse protocol_interface::{ProviderMode, ProviderType};\nuse ratatui::prelude::*;\nuse ratatui::widgets::{Block, Borders, Paragraph, Wrap};\nuse runtime_config::Settings;\nuse rust_i18n::t;\n\nuse crate::tui::app::TuiApp;\nuse crate::tui::components::{\n should_process_key_event, InputAction, ListAction, SelectableList, StepIndicator, TextInput,\n};\nuse crate::tui::theme;\n\nconst PROVIDERS: &[&str] = &[\"bigmodel\", \"xiaomi_mimo\", \"deepseek\"];\nconst LOCALES: &[(&str, &str)] = &[\n (\"en\", \"English\"),\n (\"zh-CN\", \"中文 (简体)\"),\n (\"ja\", \"日本語\"),\n];\nconst BIGMODEL_OPENAI_BASE_URL: &str = \"https://open.bigmodel.cn/api/coding/paas/v4\";\nconst BIGMODEL_ANTHROPIC_BASE_URL: &str = \"https://open.bigmodel.cn/api/anthropic\";\nconst XIAOMI_MIMO_OPENAI_BASE_URL: &str = \"https://token-plan-cn.xiaomimimo.com/v1\";\nconst XIAOMI_MIMO_OPENAI_SGP_BASE_URL: &str = \"https://token-plan-sgp.xiaomimimo.com/v1\";\nconst XIAOMI_MIMO_ANTHROPIC_BASE_URL: &str = \"https://token-plan-cn.xiaomimimo.com/anthropic\";\nconst XIAOMI_MIMO_ANTHROPIC_SGP_BASE_URL: &str = \"https://token-plan-sgp.xiaomimimo.com/anthropic\";\nconst DEEPSEEK_OPENAI_BASE_URL: &str = \"https://api.deepseek.com\";\nconst DEEPSEEK_ANTHROPIC_BASE_URL: &str = \"https://api.deepseek.com/anthropic\";\n\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\nstruct BaseUrlChoice {\n label: &'static str,\n url: Option<&'static str>,\n}\n\nimpl std::fmt::Display for BaseUrlChoice {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self.url {\n Some(url) => write!(f, \"{} ({})\", self.label, url),\n None => write!(f, \"{}\", t!(\"init.custom_base_url\")),\n }\n }\n}\n\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\nenum ConfigStep {\n Provider,\n BaseUrl,\n ApiKey,\n Model,\n Soul,\n Language,\n Confirm,\n}\n\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\nenum BaseUrlMode {\n List,\n Input,\n}\n\n#[derive(Clone)]\nenum SoulChoice {\n KeepCurrent(String),\n Formula(FormulaDef),\n}\n\nimpl std::fmt::Display for SoulChoice {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n Self::KeepCurrent(role) => write!(f, \"{}: {}\", t!(\"common.current_values\"), role),\n Self::Formula(formula) => write!(f, \"{} - {}\", formula.id, formula.description),\n }\n }\n}\n\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\nenum ConfirmChoice {\n Save,\n Cancel,\n}\n\nimpl std::fmt::Display for ConfirmChoice {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n Self::Save => write!(f, \"{}\", t!(\"common.save_exit\")),\n Self::Cancel => write!(f, \"{}\", t!(\"common.cancel\")),\n }\n }\n}\n\npub fn run_config_panel(settings: Settings) -> Result<Option<Settings>> {\n let mut draft = settings;\n let mut app = TuiApp::enter()?;\n let result = run_loop(&mut app, &mut draft);\n app.restore()?;\n let confirmed = result?;\n Ok(confirmed.then_some(draft))\n}\n\nfn run_loop(app: &mut TuiApp, settings: &mut Settings) -> Result<bool> {\n let mut step_idx = 0usize;\n let mut base_url_mode = BaseUrlMode::List;\n\n let mut provider_list = provider_list_for_settings(settings);\n let mut base_url_list = base_url_list_for_settings(settings);\n let mut base_url_input = base_url_input_for_settings(settings);\n let mut api_key_input = TextInput::new(t!(\"config.api_key_label\").to_string())\n .with_default(settings.llm.api_key.as_deref().unwrap_or_default());\n let mut model_input =\n TextInput::new(t!(\"config.model_label\").to_string()).with_default(&settings.llm.model);\n let soul_formulas = crate::formula::list_installed_souls().unwrap_or_default();\n let mut soul_list = soul_list_for_settings(settings, &soul_formulas);\n let mut language_list = language_list_for_settings(settings);\n let mut confirm_list = build_confirm_list();\n\n loop {\n let steps = build_steps(&settings.llm.provider);\n if step_idx >= steps.len() {\n step_idx = steps.len().saturating_sub(1);\n }\n let current_step = steps[step_idx];\n\n app.draw(|frame| {\n let area = frame.area();\n let block = Block::default()\n .borders(Borders::ALL)\n .title(Span::styled(\n format!(\" {} \", t!(\"config.title\")),\n theme::title(),\n ))\n .style(theme::base())\n .border_style(theme::border(true))\n .title_alignment(Alignment::Center);\n let inner = block.inner(area);\n frame.render_widget(block, area);\n\n render_step_header(frame, inner, &steps, step_idx, current_step, settings);\n\n let content_area = Rect::new(\n inner.x + 2,\n inner.y + 6,\n inner.width.saturating_sub(4),\n inner.height.saturating_sub(8),\n );\n let help_area = Rect::new(\n inner.x + 2,\n inner.y + inner.height.saturating_sub(1),\n inner.width.saturating_sub(4),\n 1,\n );\n\n match current_step {\n ConfigStep::Provider => provider_list.render(frame, content_area),\n ConfigStep::BaseUrl => match base_url_mode {\n BaseUrlMode::List => base_url_list.render(frame, content_area),\n BaseUrlMode::Input => render_input(frame, content_area, &base_url_input),\n },\n ConfigStep::ApiKey => render_input(frame, content_area, &api_key_input),\n ConfigStep::Model => render_input(frame, content_area, &model_input),\n ConfigStep::Soul => soul_list.render(frame, content_area),\n ConfigStep::Language => language_list.render(frame, content_area),\n ConfigStep::Confirm => {\n render_confirmation(frame, content_area, settings, &mut confirm_list)\n }\n }\n\n let help_text = if step_idx == 0 {\n t!(\"init.help.first_step\").to_string()\n } else {\n t!(\"init.help.next_step\").to_string()\n };\n frame.render_widget(\n Paragraph::new(Span::styled(help_text, theme::secondary())),\n help_area,\n );\n })?;\n\n if event::poll(std::time::Duration::from_millis(100))? {\n match event::read()? {\n Event::Paste(text) => match current_step {\n ConfigStep::BaseUrl if base_url_mode == BaseUrlMode::Input => {\n base_url_input.paste(&text);\n }\n ConfigStep::ApiKey => api_key_input.paste(&text),\n ConfigStep::Model => model_input.paste(&text),\n _ => {}\n },\n Event::Key(key) => {\n if !should_process_key_event(&key) {\n continue;\n }\n\n if key.code == KeyCode::Backspace\n && key.modifiers.contains(KeyModifiers::CONTROL)\n {\n if current_step == ConfigStep::BaseUrl\n && base_url_mode == BaseUrlMode::Input\n {\n base_url_mode = BaseUrlMode::List;\n } else if !go_back(&mut step_idx) {\n return Ok(false);\n }\n continue;\n }\n\n match current_step {\n ConfigStep::Provider => match provider_list.handle_key(key) {\n ListAction::Select => {\n if let Some(selected) = provider_list.selected() {\n let provider = parse_provider(selected);\n if provider != settings.llm.provider {\n settings.llm.provider = provider;\n settings.llm.provider_mode = None;\n settings.llm.base_url = None;\n }\n base_url_list = base_url_list_for_settings(settings);\n base_url_input = base_url_input_for_settings(settings);\n base_url_mode = BaseUrlMode::List;\n step_idx += 1;\n }\n }\n ListAction::Cancel => return Ok(false),\n ListAction::None => {}\n },\n ConfigStep::BaseUrl => match base_url_mode {\n BaseUrlMode::List => match base_url_list.handle_key(key) {\n ListAction::Select => {\n if let Some(selected) = base_url_list.selected() {\n if let Some(url) = selected.url {\n settings.llm.base_url = Some(url.to_string());\n settings.llm.provider_mode =\n provider_mode_for_base_url(url);\n base_url_input = base_url_input_for_settings(settings);\n step_idx += 1;\n } else {\n base_url_input = base_url_input_for_settings(settings);\n base_url_mode = BaseUrlMode::Input;\n }\n }\n }\n ListAction::Cancel => {\n if !go_back(&mut step_idx) {\n return Ok(false);\n }\n }\n ListAction::None => {}\n },\n BaseUrlMode::Input => match base_url_input.handle_key(key) {\n InputAction::Submit => {\n let base_url = base_url_input.value().to_string();\n settings.llm.provider_mode =\n provider_mode_for_base_url(&base_url);\n settings.llm.base_url = Some(base_url);\n base_url_list = base_url_list_for_settings(settings);\n base_url_mode = BaseUrlMode::List;\n step_idx += 1;\n }\n InputAction::Cancel => {\n base_url_mode = BaseUrlMode::List;\n }\n InputAction::None => {}\n },\n },\n ConfigStep::ApiKey => match api_key_input.handle_key(key) {\n InputAction::Submit => {\n let val = api_key_input.value().to_string();\n settings.llm.api_key =\n if val.is_empty() { None } else { Some(val) };\n step_idx += 1;\n }\n InputAction::Cancel => {\n if !go_back(&mut step_idx) {\n return Ok(false);\n }\n }\n InputAction::None => {}\n },\n ConfigStep::Model => match model_input.handle_key(key) {\n InputAction::Submit => {\n settings.llm.model = model_input.value().to_string();\n step_idx += 1;\n }\n InputAction::Cancel => {\n if !go_back(&mut step_idx) {\n return Ok(false);\n }\n }\n InputAction::None => {}\n },\n ConfigStep::Soul => match soul_list.handle_key(key) {\n ListAction::Select => {\n if let Some(selected) = soul_list.selected() {\n match selected {\n SoulChoice::KeepCurrent(_) => {}\n SoulChoice::Formula(formula) => {\n let _ = crate::formula::activate_soul(&formula.id);\n settings.soul.role = protocol_interface::SoulRole::new(\n formula.id.clone(),\n );\n }\n }\n soul_list = soul_list_for_settings(settings, &soul_formulas);\n step_idx += 1;\n }\n }\n ListAction::Cancel => {\n if !go_back(&mut step_idx) {\n return Ok(false);\n }\n }\n ListAction::None => {}\n },\n ConfigStep::Language => match language_list.handle_key(key) {\n ListAction::Select => {\n if let Some(selected) = language_list.selected() {\n let new_locale = locale_from_display(selected);\n settings.ui.locale = new_locale.clone();\n crate::set_locale(&new_locale);\n crate::tui::theme::set_theme(&settings.ui.theme);\n provider_list = provider_list_for_settings(settings);\n base_url_list = base_url_list_for_settings(settings);\n base_url_input = base_url_input_for_settings(settings);\n api_key_input =\n TextInput::new(t!(\"config.api_key_label\").to_string())\n .with_default(\n settings.llm.api_key.as_deref().unwrap_or_default(),\n );\n model_input =\n TextInput::new(t!(\"config.model_label\").to_string())\n .with_default(&settings.llm.model);\n soul_list = soul_list_for_settings(settings, &soul_formulas);\n language_list = language_list_for_settings(settings);\n confirm_list = build_confirm_list();\n step_idx += 1;\n }\n }\n ListAction::Cancel => {\n if !go_back(&mut step_idx) {\n return Ok(false);\n }\n }\n ListAction::None => {}\n },\n ConfigStep::Confirm => match confirm_list.handle_key(key) {\n ListAction::Select => {\n if let Some(selected) = confirm_list.selected() {\n match selected {\n ConfirmChoice::Save => {\n settings.save_to_user_config()?;\n return Ok(true);\n }\n ConfirmChoice::Cancel => return Ok(false),\n }\n }\n }\n ListAction::Cancel => {\n if !go_back(&mut step_idx) {\n return Ok(false);\n }\n }\n ListAction::None => {}\n },\n }\n }\n _ => {}\n }\n }\n }\n}\n\nfn render_step_header(\n frame: &mut ratatui::Frame,\n area: Rect,\n steps: &[ConfigStep],\n step_idx: usize,\n current_step: ConfigStep,\n settings: &Settings,\n) {\n let indicator_area = Rect::new(area.x + 2, area.y + 1, area.width.saturating_sub(4), 1);\n let mut indicator =\n StepIndicator::new(steps.iter().map(|step| step_display_name(*step)).collect());\n indicator.set_current(step_idx);\n indicator.render(frame, indicator_area);\n\n let label_area = Rect::new(area.x + 2, area.y + 3, area.width.saturating_sub(4), 1);\n let label = t!(\n \"init.step_label\",\n current = step_idx + 1,\n total = steps.len(),\n name = step_display_name(current_step)\n );\n frame.render_widget(\n Paragraph::new(Span::styled(label.to_string(), theme::emphasis())),\n label_area,\n );\n\n if let Some(value) = current_value_for_step(current_step, settings) {\n let current_area = Rect::new(area.x + 2, area.y + 4, area.width.saturating_sub(4), 1);\n frame.render_widget(\n Paragraph::new(Span::styled(value, theme::secondary())),\n current_area,\n );\n }\n}\n\nfn render_input(frame: &mut ratatui::Frame, area: Rect, input: &TextInput) {\n let mut display = input.clone();\n display.set_active(true);\n let input_area = Rect::new(area.x, area.y, area.width, TextInput::HEIGHT);\n display.render(frame, input_area);\n}\n\nfn render_confirmation(\n frame: &mut ratatui::Frame,\n area: Rect,\n settings: &Settings,\n confirm_list: &mut SelectableList<ConfirmChoice>,\n) {\n let summary_height = (area.height / 2).max(6);\n let summary_area = Rect::new(area.x, area.y, area.width, summary_height.min(area.height));\n let list_y = summary_area.y + summary_area.height.saturating_add(1);\n let list_area = Rect::new(\n area.x,\n list_y,\n area.width,\n area.y.saturating_add(area.height).saturating_sub(list_y),\n );\n\n let summary = Paragraph::new(format_settings_summary(settings))\n .style(theme::text())\n .wrap(Wrap { trim: false });\n frame.render_widget(summary, summary_area);\n confirm_list.render(frame, list_area);\n}\n\nfn build_steps(_provider: &ProviderType) -> Vec<ConfigStep> {\n let mut steps = vec![ConfigStep::Provider];\n steps.extend_from_slice(&[\n ConfigStep::BaseUrl,\n ConfigStep::ApiKey,\n ConfigStep::Model,\n ConfigStep::Soul,\n ConfigStep::Language,\n ConfigStep::Confirm,\n ]);\n steps\n}\n\nfn step_display_name(step: ConfigStep) -> String {\n match step {\n ConfigStep::Provider => t!(\"init.step.provider\").to_string(),\n ConfigStep::BaseUrl => t!(\"init.step.base_url\").to_string(),\n ConfigStep::ApiKey => t!(\"init.step.api_key\").to_string(),\n ConfigStep::Model => t!(\"init.step.model\").to_string(),\n ConfigStep::Soul => t!(\"init.step.soul\").to_string(),\n ConfigStep::Language => t!(\"init.step.language\").to_string(),\n ConfigStep::Confirm => t!(\"common.save\").to_string(),\n }\n}\n\nfn current_value_for_step(step: ConfigStep, settings: &Settings) -> Option<String> {\n let value = match step {\n ConfigStep::Provider => format!(\"{:?}\", settings.llm.provider),\n ConfigStep::BaseUrl => settings.effective_base_url(),\n ConfigStep::ApiKey => {\n if let Some(key) = settings.llm.api_key.as_deref() {\n key.to_string()\n } else {\n t!(\"common.not_set\").to_string()\n }\n }\n ConfigStep::Model => settings.llm.model.clone(),\n ConfigStep::Soul => current_soul(settings),\n ConfigStep::Language => locale_display(&settings.ui.locale),\n ConfigStep::Confirm => return None,\n };\n Some(format!(\"{}: {}\", t!(\"common.current_values\"), value))\n}\n\nfn format_settings_summary(settings: &Settings) -> String {\n let api_key_status = if let Some(key) = settings.llm.api_key.as_deref() {\n key.to_string()\n } else {\n t!(\"common.not_set\").to_string()\n };\n let provider = format!(\"{:?}\", settings.llm.provider);\n\n format!(\n \"{}\\n\\n\\\n {} {}\\n\\\n {} {}\\n\\\n {} {}\\n\\\n {} {}\\n\\\n {} {}\\n\\\n {} {}\\n\\\n {} {}\",\n t!(\"common.current_values\"),\n t!(\"config.show_provider\"),\n provider,\n t!(\"init.step.mode\"),\n mode_display(settings),\n t!(\"config.show_base_url\"),\n settings.effective_base_url(),\n t!(\"config.show_api_key\"),\n api_key_status,\n t!(\"config.show_model\"),\n settings.llm.model,\n t!(\"config.show_soul\"),\n current_soul(settings),\n t!(\"config.show_language\"),\n locale_display(&settings.ui.locale),\n )\n}\n\nfn provider_list_for_settings(settings: &Settings) -> SelectableList<String> {\n let current = provider_key(&settings.llm.provider);\n let mut list = SelectableList::new(\n t!(\"config.select_provider\").to_string(),\n PROVIDERS\n .iter()\n .map(|provider| provider.to_string())\n .collect(),\n );\n if let Some(idx) = PROVIDERS.iter().position(|provider| *provider == current) {\n select_index(&mut list, idx);\n }\n list\n}\n\nfn base_url_list_for_settings(settings: &Settings) -> SelectableList<BaseUrlChoice> {\n let choices = base_url_choices(&settings.llm.provider, &settings.llm.provider_mode);\n let selected_idx = settings\n .llm\n .base_url\n .as_deref()\n .and_then(|url| {\n choices.iter().position(|choice| {\n choice\n .url\n .map(|known_url| known_url.trim() == url.trim())\n .unwrap_or(false)\n })\n })\n .unwrap_or_else(|| {\n if settings.llm.base_url.is_some() {\n choices\n .iter()\n .position(|choice| choice.url.is_none())\n .unwrap_or(0)\n } else {\n 0\n }\n });\n\n let mut list = SelectableList::new(t!(\"config.select_base_url\").to_string(), choices);\n select_index(&mut list, selected_idx);\n list\n}\n\nfn base_url_input_for_settings(settings: &Settings) -> TextInput {\n let input = TextInput::new(t!(\"config.base_url_label\").to_string());\n if let Some(base_url) = editable_custom_base_url(settings) {\n input.with_default(base_url)\n } else {\n input.with_placeholder(&settings.effective_base_url())\n }\n}\n\nfn editable_custom_base_url(settings: &Settings) -> Option<&str> {\n let base_url = settings.llm.base_url.as_deref()?.trim();\n if base_url.is_empty()\n || is_builtin_base_url(\n base_url,\n &settings.llm.provider,\n &settings.llm.provider_mode,\n )\n {\n None\n } else {\n Some(base_url)\n }\n}\n\nfn is_builtin_base_url(\n url: &str,\n provider: &ProviderType,\n provider_mode: &Option<ProviderMode>,\n) -> bool {\n base_url_choices(provider, provider_mode)\n .iter()\n .filter_map(|choice| choice.url)\n .any(|known_url| known_url.trim() == url.trim())\n}\n\nfn base_url_choices(\n provider: &ProviderType,\n _provider_mode: &Option<ProviderMode>,\n) -> Vec<BaseUrlChoice> {\n match provider {\n ProviderType::BigModel => vec![\n BaseUrlChoice {\n label: \"OpenAI API\",\n url: Some(BIGMODEL_OPENAI_BASE_URL),\n },\n BaseUrlChoice {\n label: \"Anthropic API\",\n url: Some(BIGMODEL_ANTHROPIC_BASE_URL),\n },\n BaseUrlChoice {\n label: \"Custom\",\n url: None,\n },\n ],\n ProviderType::XiaomiMimo => vec![\n BaseUrlChoice {\n label: \"OpenAI API (China)\",\n url: Some(XIAOMI_MIMO_OPENAI_BASE_URL),\n },\n BaseUrlChoice {\n label: \"OpenAI API (Singapore)\",\n url: Some(XIAOMI_MIMO_OPENAI_SGP_BASE_URL),\n },\n BaseUrlChoice {\n label: \"Anthropic API (China)\",\n url: Some(XIAOMI_MIMO_ANTHROPIC_BASE_URL),\n },\n BaseUrlChoice {\n label: \"Anthropic API (Singapore)\",\n url: Some(XIAOMI_MIMO_ANTHROPIC_SGP_BASE_URL),\n },\n BaseUrlChoice {\n label: \"Custom\",\n url: None,\n },\n ],\n ProviderType::DeepSeek => vec![\n BaseUrlChoice {\n label: \"OpenAI API\",\n url: Some(DEEPSEEK_OPENAI_BASE_URL),\n },\n BaseUrlChoice {\n label: \"Anthropic API\",\n url: Some(DEEPSEEK_ANTHROPIC_BASE_URL),\n },\n BaseUrlChoice {\n label: \"Custom\",\n url: None,\n },\n ],\n ProviderType::Custom => vec![BaseUrlChoice {\n label: \"Custom\",\n url: None,\n }],\n _ => vec![BaseUrlChoice {\n label: \"Custom\",\n url: None,\n }],\n }\n}\n\nfn soul_list_for_settings(\n settings: &Settings,\n formulas: &[FormulaDef],\n) -> SelectableList<SoulChoice> {\n let current = current_soul(settings);\n let mut choices = vec![SoulChoice::KeepCurrent(current)];\n choices.extend(formulas.iter().cloned().map(SoulChoice::Formula));\n\n SelectableList::new(t!(\"config.select_soul\").to_string(), choices)\n}\n\nfn language_list_for_settings(settings: &Settings) -> SelectableList<String> {\n let locale_items: Vec<String> = LOCALES\n .iter()\n .map(|(code, name)| format!(\"{} - {}\", name, code))\n .collect();\n let mut list = SelectableList::new(t!(\"config.select_language\").to_string(), locale_items);\n if let Some(idx) = LOCALES\n .iter()\n .position(|(code, _)| *code == settings.ui.locale)\n {\n select_index(&mut list, idx);\n }\n list\n}\n\nfn build_confirm_list() -> SelectableList<ConfirmChoice> {\n SelectableList::new(\n t!(\"config.confirm_title\").to_string(),\n vec![ConfirmChoice::Save, ConfirmChoice::Cancel],\n )\n}\n\nfn parse_provider(provider: &str) -> ProviderType {\n match provider {\n \"bigmodel\" => ProviderType::BigModel,\n \"xiaomi_mimo\" => ProviderType::XiaomiMimo,\n \"deepseek\" => ProviderType::DeepSeek,\n _ => ProviderType::BigModel,\n }\n}\n\nfn provider_key(provider: &ProviderType) -> &'static str {\n match provider {\n ProviderType::BigModel => \"bigmodel\",\n ProviderType::XiaomiMimo => \"xiaomi_mimo\",\n ProviderType::DeepSeek => \"deepseek\",\n _ => \"bigmodel\",\n }\n}\n\nfn provider_mode_for_base_url(base_url: &str) -> Option<ProviderMode> {\n if base_url.contains(\"/anthropic\") {\n Some(ProviderMode::Native)\n } else {\n Some(ProviderMode::OpenAICompatible)\n }\n}\n\nfn locale_from_display(display: &str) -> String {\n LOCALES\n .iter()\n .find_map(|(code, name)| {\n if display.starts_with(name) {\n Some(code.to_string())\n } else {\n None\n }\n })\n .unwrap_or_else(|| \"en\".to_string())\n}\n\nfn locale_display(locale: &str) -> String {\n LOCALES\n .iter()\n .find(|(code, _)| *code == locale)\n .map(|(_, name)| name.to_string())\n .unwrap_or_else(|| locale.to_string())\n}\n\nfn current_soul(settings: &Settings) -> String {\n crate::formula::current_project_soul().unwrap_or_else(|| settings.soul.role.to_string())\n}\n\nfn mode_display(settings: &Settings) -> String {\n match settings.llm.provider_mode {\n Some(ProviderMode::Native) => \"Anthropic API\".to_string(),\n Some(ProviderMode::OpenAICompatible) => \"OpenAI API\".to_string(),\n _ if settings.effective_base_url().contains(\"/anthropic\") => \"Anthropic API\".to_string(),\n _ => \"OpenAI API\".to_string(),\n }\n}\n\nfn go_back(step_idx: &mut usize) -> bool {\n if *step_idx > 0 {\n *step_idx -= 1;\n true\n } else {\n false\n }\n}\n\nfn select_index<T: Clone + std::fmt::Display>(list: &mut SelectableList<T>, idx: usize) {\n for _ in 0..idx {\n let _ = list.handle_key(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n fn test_settings() -> Settings {\n Settings::default()\n }\n\n #[test]\n fn test_build_steps_openai() {\n let steps = build_steps(&ProviderType::Openai);\n assert_eq!(\n steps,\n vec![\n ConfigStep::Provider,\n ConfigStep::BaseUrl,\n ConfigStep::ApiKey,\n ConfigStep::Model,\n ConfigStep::Soul,\n ConfigStep::Language,\n ConfigStep::Confirm,\n ]\n );\n }\n\n #[test]\n fn test_build_steps_bigmodel_includes_mode() {\n let steps = build_steps(&ProviderType::BigModel);\n assert_eq!(steps[0], ConfigStep::Provider);\n assert_eq!(steps[1], ConfigStep::BaseUrl);\n assert_eq!(steps.last(), Some(&ConfigStep::Confirm));\n }\n\n #[test]\n fn test_provider_options() {\n assert_eq!(PROVIDERS.len(), 3);\n assert!(PROVIDERS.contains(&\"bigmodel\"));\n assert!(PROVIDERS.contains(&\"xiaomi_mimo\"));\n assert!(PROVIDERS.contains(&\"deepseek\"));\n }\n\n #[test]\n fn test_provider_parse() {\n let p = parse_provider(\"bigmodel\");\n assert!(matches!(p, ProviderType::BigModel));\n }\n\n #[test]\n fn test_bigmodel_base_url_options() {\n let choices = base_url_choices(&ProviderType::BigModel, &None);\n assert_eq!(choices.len(), 3);\n assert_eq!(\n choices[0].url,\n Some(\"https://open.bigmodel.cn/api/coding/paas/v4\")\n );\n assert_eq!(\n choices[1].url,\n Some(\"https://open.bigmodel.cn/api/anthropic\")\n );\n assert_eq!(choices[2].url, None);\n }\n\n #[test]\n fn test_deepseek_base_url_options() {\n let choices = base_url_choices(&ProviderType::DeepSeek, &None);\n assert_eq!(choices.len(), 3);\n assert_eq!(choices[0].url, Some(\"https://api.deepseek.com\"));\n assert_eq!(choices[1].url, Some(\"https://api.deepseek.com/anthropic\"));\n }\n\n #[test]\n fn test_xiaomi_base_url_options_include_regions() {\n let choices = base_url_choices(&ProviderType::XiaomiMimo, &None);\n\n assert_eq!(choices.len(), 5);\n assert_eq!(\n choices[0].url,\n Some(\"https://token-plan-cn.xiaomimimo.com/v1\")\n );\n assert_eq!(\n choices[1].url,\n Some(\"https://token-plan-sgp.xiaomimimo.com/v1\")\n );\n assert_eq!(\n choices[2].url,\n Some(\"https://token-plan-cn.xiaomimimo.com/anthropic\")\n );\n assert_eq!(\n choices[3].url,\n Some(\"https://token-plan-sgp.xiaomimimo.com/anthropic\")\n );\n assert_eq!(choices[4].url, None);\n }\n\n #[test]\n fn test_base_url_input_starts_empty_for_builtin_default() {\n let mut settings = test_settings();\n settings.llm.provider = ProviderType::BigModel;\n settings.llm.base_url = Some(BIGMODEL_OPENAI_BASE_URL.to_string());\n\n let input = base_url_input_for_settings(&settings);\n\n assert_eq!(input.value(), \"\");\n }\n\n #[test]\n fn test_base_url_input_preserves_existing_custom_url() {\n let mut settings = test_settings();\n settings.llm.provider = ProviderType::BigModel;\n settings.llm.base_url = Some(\"http://localhost:11434/v1\".to_string());\n\n let input = base_url_input_for_settings(&settings);\n\n assert_eq!(input.value(), \"http://localhost:11434/v1\");\n }\n\n #[test]\n fn test_settings_default_values() {\n let s = test_settings();\n assert_eq!(s.llm.model, \"gpt-4o-mini\");\n assert!(s.llm.api_key.is_none());\n }\n\n #[test]\n fn test_soul_choices_keep_current() {\n let settings = test_settings();\n let list = soul_list_for_settings(&settings, &[]);\n\n assert!(matches!(list.selected(), Some(SoulChoice::KeepCurrent(_))));\n }\n\n #[test]\n fn test_language_parse() {\n assert_eq!(locale_from_display(\"中文 (简体) - zh-CN\"), \"zh-CN\");\n assert_eq!(locale_from_display(\"English - en\"), \"en\");\n }\n}\n"}, "ed9ccbca2fdad3a89ac73177ea2f8cd0ba05cb51": {"t": 1785750731.1499712, "n": 32211, "h": "de6be286caec221f", "c": "use crate::formula::FormulaDef;\nuse anyhow::Result;\nuse crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};\nuse protocol_interface::{ProviderMode, ProviderType};\nuse ratatui::prelude::*;\nuse ratatui::widgets::{Block, Borders, Paragraph, Wrap};\nuse runtime_config::Settings;\nuse rust_i18n::t;\n\nuse crate::tui::app::TuiApp;\nuse crate::tui::components::{\n should_process_key_event, InputAction, ListAction, SelectableList, StepIndicator, TextInput,\n};\nuse crate::tui::theme;\n\nconst PROVIDERS: &[&str] = &[\"bigmodel\", \"xiaomi_mimo\", \"deepseek\"];\nconst LOCALES: &[(&str, &str)] = &[\n (\"en\", \"English\"),\n (\"zh-CN\", \"中文 (简体)\"),\n (\"ja\", \"日本語\"),\n];\nconst BIGMODEL_OPENAI_BASE_URL: &str = \"https://open.bigmodel.cn/api/coding/paas/v4\";\nconst BIGMODEL_ANTHROPIC_BASE_URL: &str = \"https://open.bigmodel.cn/api/anthropic\";\nconst XIAOMI_MIMO_OPENAI_BASE_URL: &str = \"https://api.xiaomimimo.com/v1\";\nconst XIAOMI_MIMO_ANTHROPIC_BASE_URL: &str = \"https://api.xiaomimimo.com/anthropic\";\nconst DEEPSEEK_OPENAI_BASE_URL: &str = \"https://api.deepseek.com\";\nconst DEEPSEEK_ANTHROPIC_BASE_URL: &str = \"https://api.deepseek.com/anthropic\";\n\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\nstruct BaseUrlChoice {\n label: &'static str,\n url: Option<&'static str>,\n}\n\nimpl std::fmt::Display for BaseUrlChoice {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self.url {\n Some(url) => write!(f, \"{} ({})\", self.label, url),\n None => write!(f, \"{}\", t!(\"init.custom_base_url\")),\n }\n }\n}\n\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\nenum ConfigStep {\n Provider,\n BaseUrl,\n ApiKey,\n Model,\n Soul,\n Language,\n Confirm,\n}\n\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\nenum BaseUrlMode {\n List,\n Input,\n}\n\n#[derive(Clone)]\nenum SoulChoice {\n KeepCurrent(String),\n Formula(FormulaDef),\n}\n\nimpl std::fmt::Display for SoulChoice {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n Self::KeepCurrent(role) => write!(f, \"{}: {}\", t!(\"common.current_values\"), role),\n Self::Formula(formula) => write!(f, \"{} - {}\", formula.id, formula.description),\n }\n }\n}\n\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\nenum ConfirmChoice {\n Save,\n Cancel,\n}\n\nimpl std::fmt::Display for ConfirmChoice {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n Self::Save => write!(f, \"{}\", t!(\"common.save_exit\")),\n Self::Cancel => write!(f, \"{}\", t!(\"common.cancel\")),\n }\n }\n}\n\npub fn run_config_panel(settings: Settings) -> Result<Option<Settings>> {\n let mut draft = settings;\n let mut app = TuiApp::enter()?;\n let result = run_loop(&mut app, &mut draft);\n app.restore()?;\n let confirmed = result?;\n Ok(confirmed.then_some(draft))\n}\n\nfn run_loop(app: &mut TuiApp, settings: &mut Settings) -> Result<bool> {\n let mut step_idx = 0usize;\n let mut base_url_mode = BaseUrlMode::List;\n\n let mut provider_list = provider_list_for_settings(settings);\n let mut base_url_list = base_url_list_for_settings(settings);\n let mut base_url_input = base_url_input_for_settings(settings);\n let mut api_key_input = TextInput::new(t!(\"config.api_key_label\").to_string())\n .with_default(settings.llm.api_key.as_deref().unwrap_or_default());\n let mut model_input =\n TextInput::new(t!(\"config.model_label\").to_string()).with_default(&settings.llm.model);\n let soul_formulas = crate::formula::list_installed_souls().unwrap_or_default();\n let mut soul_list = soul_list_for_settings(settings, &soul_formulas);\n let mut language_list = language_list_for_settings(settings);\n let mut confirm_list = build_confirm_list();\n\n loop {\n let steps = build_steps(&settings.llm.provider);\n if step_idx >= steps.len() {\n step_idx = steps.len().saturating_sub(1);\n }\n let current_step = steps[step_idx];\n\n app.draw(|frame| {\n let area = frame.area();\n let block = Block::default()\n .borders(Borders::ALL)\n .title(Span::styled(\n format!(\" {} \", t!(\"config.title\")),\n theme::title(),\n ))\n .style(theme::base())\n .border_style(theme::border(true))\n .title_alignment(Alignment::Center);\n let inner = block.inner(area);\n frame.render_widget(block, area);\n\n render_step_header(frame, inner, &steps, step_idx, current_step, settings);\n\n let content_area = Rect::new(\n inner.x + 2,\n inner.y + 6,\n inner.width.saturating_sub(4),\n inner.height.saturating_sub(8),\n );\n let help_area = Rect::new(\n inner.x + 2,\n inner.y + inner.height.saturating_sub(1),\n inner.width.saturating_sub(4),\n 1,\n );\n\n match current_step {\n ConfigStep::Provider => provider_list.render(frame, content_area),\n ConfigStep::BaseUrl => match base_url_mode {\n BaseUrlMode::List => base_url_list.render(frame, content_area),\n BaseUrlMode::Input => render_input(frame, content_area, &base_url_input),\n },\n ConfigStep::ApiKey => render_input(frame, content_area, &api_key_input),\n ConfigStep::Model => render_input(frame, content_area, &model_input),\n ConfigStep::Soul => soul_list.render(frame, content_area),\n ConfigStep::Language => language_list.render(frame, content_area),\n ConfigStep::Confirm => {\n render_confirmation(frame, content_area, settings, &mut confirm_list)\n }\n }\n\n let help_text = if step_idx == 0 {\n t!(\"init.help.first_step\").to_string()\n } else {\n t!(\"init.help.next_step\").to_string()\n };\n frame.render_widget(\n Paragraph::new(Span::styled(help_text, theme::secondary())),\n help_area,\n );\n })?;\n\n if event::poll(std::time::Duration::from_millis(100))? {\n match event::read()? {\n Event::Paste(text) => match current_step {\n ConfigStep::BaseUrl if base_url_mode == BaseUrlMode::Input => {\n base_url_input.paste(&text);\n }\n ConfigStep::ApiKey => api_key_input.paste(&text),\n ConfigStep::Model => model_input.paste(&text),\n _ => {}\n },\n Event::Key(key) => {\n if !should_process_key_event(&key) {\n continue;\n }\n\n if key.code == KeyCode::Backspace\n && key.modifiers.contains(KeyModifiers::CONTROL)\n {\n if current_step == ConfigStep::BaseUrl\n && base_url_mode == BaseUrlMode::Input\n {\n base_url_mode = BaseUrlMode::List;\n } else if !go_back(&mut step_idx) {\n return Ok(false);\n }\n continue;\n }\n\n match current_step {\n ConfigStep::Provider => match provider_list.handle_key(key) {\n ListAction::Select => {\n if let Some(selected) = provider_list.selected() {\n let provider = parse_provider(selected);\n if provider != settings.llm.provider {\n settings.llm.provider = provider;\n settings.llm.provider_mode = None;\n settings.llm.base_url = None;\n }\n base_url_list = base_url_list_for_settings(settings);\n base_url_input = base_url_input_for_settings(settings);\n base_url_mode = BaseUrlMode::List;\n step_idx += 1;\n }\n }\n ListAction::Cancel => return Ok(false),\n ListAction::None => {}\n },\n ConfigStep::BaseUrl => match base_url_mode {\n BaseUrlMode::List => match base_url_list.handle_key(key) {\n ListAction::Select => {\n if let Some(selected) = base_url_list.selected() {\n if let Some(url) = selected.url {\n settings.llm.base_url = Some(url.to_string());\n settings.llm.provider_mode =\n provider_mode_for_base_url(url);\n base_url_input = base_url_input_for_settings(settings);\n step_idx += 1;\n } else {\n base_url_input = base_url_input_for_settings(settings);\n base_url_mode = BaseUrlMode::Input;\n }\n }\n }\n ListAction::Cancel => {\n if !go_back(&mut step_idx) {\n return Ok(false);\n }\n }\n ListAction::None => {}\n },\n BaseUrlMode::Input => match base_url_input.handle_key(key) {\n InputAction::Submit => {\n let base_url = base_url_input.value().to_string();\n settings.llm.provider_mode =\n provider_mode_for_base_url(&base_url);\n settings.llm.base_url = Some(base_url);\n base_url_list = base_url_list_for_settings(settings);\n base_url_mode = BaseUrlMode::List;\n step_idx += 1;\n }\n InputAction::Cancel => {\n base_url_mode = BaseUrlMode::List;\n }\n InputAction::None => {}\n },\n },\n ConfigStep::ApiKey => match api_key_input.handle_key(key) {\n InputAction::Submit => {\n let val = api_key_input.value().to_string();\n settings.llm.api_key =\n if val.is_empty() { None } else { Some(val) };\n step_idx += 1;\n }\n InputAction::Cancel => {\n if !go_back(&mut step_idx) {\n return Ok(false);\n }\n }\n InputAction::None => {}\n },\n ConfigStep::Model => match model_input.handle_key(key) {\n InputAction::Submit => {\n settings.llm.model = model_input.value().to_string();\n step_idx += 1;\n }\n InputAction::Cancel => {\n if !go_back(&mut step_idx) {\n return Ok(false);\n }\n }\n InputAction::None => {}\n },\n ConfigStep::Soul => match soul_list.handle_key(key) {\n ListAction::Select => {\n if let Some(selected) = soul_list.selected() {\n match selected {\n SoulChoice::KeepCurrent(_) => {}\n SoulChoice::Formula(formula) => {\n let _ = crate::formula::activate_soul(&formula.id);\n settings.soul.role = protocol_interface::SoulRole::new(\n formula.id.clone(),\n );\n }\n }\n soul_list = soul_list_for_settings(settings, &soul_formulas);\n step_idx += 1;\n }\n }\n ListAction::Cancel => {\n if !go_back(&mut step_idx) {\n return Ok(false);\n }\n }\n ListAction::None => {}\n },\n ConfigStep::Language => match language_list.handle_key(key) {\n ListAction::Select => {\n if let Some(selected) = language_list.selected() {\n let new_locale = locale_from_display(selected);\n settings.ui.locale = new_locale.clone();\n crate::set_locale(&new_locale);\n provider_list = provider_list_for_settings(settings);\n base_url_list = base_url_list_for_settings(settings);\n base_url_input = base_url_input_for_settings(settings);\n api_key_input =\n TextInput::new(t!(\"config.api_key_label\").to_string())\n .with_default(\n settings.llm.api_key.as_deref().unwrap_or_default(),\n );\n model_input =\n TextInput::new(t!(\"config.model_label\").to_string())\n .with_default(&settings.llm.model);\n soul_list = soul_list_for_settings(settings, &soul_formulas);\n language_list = language_list_for_settings(settings);\n confirm_list = build_confirm_list();\n step_idx += 1;\n }\n }\n ListAction::Cancel => {\n if !go_back(&mut step_idx) {\n return Ok(false);\n }\n }\n ListAction::None => {}\n },\n ConfigStep::Confirm => match confirm_list.handle_key(key) {\n ListAction::Select => {\n if let Some(selected) = confirm_list.selected() {\n match selected {\n ConfirmChoice::Save => {\n settings.save_to_user_config()?;\n return Ok(true);\n }\n ConfirmChoice::Cancel => return Ok(false),\n }\n }\n }\n ListAction::Cancel => {\n if !go_back(&mut step_idx) {\n return Ok(false);\n }\n }\n ListAction::None => {}\n },\n }\n }\n _ => {}\n }\n }\n }\n}\n\nfn render_step_header(\n frame: &mut ratatui::Frame,\n area: Rect,\n steps: &[ConfigStep],\n step_idx: usize,\n current_step: ConfigStep,\n settings: &Settings,\n) {\n let indicator_area = Rect::new(area.x + 2, area.y + 1, area.width.saturating_sub(4), 1);\n let mut indicator =\n StepIndicator::new(steps.iter().map(|step| step_display_name(*step)).collect());\n indicator.set_current(step_idx);\n indicator.render(frame, indicator_area);\n\n let label_area = Rect::new(area.x + 2, area.y + 3, area.width.saturating_sub(4), 1);\n let label = t!(\n \"init.step_label\",\n current = step_idx + 1,\n total = steps.len(),\n name = step_display_name(current_step)\n );\n frame.render_widget(\n Paragraph::new(Span::styled(label.to_string(), theme::emphasis())),\n label_area,\n );\n\n if let Some(value) = current_value_for_step(current_step, settings) {\n let current_area = Rect::new(area.x + 2, area.y + 4, area.width.saturating_sub(4), 1);\n frame.render_widget(\n Paragraph::new(Span::styled(value, theme::secondary())),\n current_area,\n );\n }\n}\n\nfn render_input(frame: &mut ratatui::Frame, area: Rect, input: &TextInput) {\n let mut display = input.clone();\n display.set_active(true);\n let input_area = Rect::new(area.x, area.y, area.width, TextInput::HEIGHT);\n display.render(frame, input_area);\n}\n\nfn render_confirmation(\n frame: &mut ratatui::Frame,\n area: Rect,\n settings: &Settings,\n confirm_list: &mut SelectableList<ConfirmChoice>,\n) {\n let summary_height = (area.height / 2).max(6);\n let summary_area = Rect::new(area.x, area.y, area.width, summary_height.min(area.height));\n let list_y = summary_area.y + summary_area.height.saturating_add(1);\n let list_area = Rect::new(\n area.x,\n list_y,\n area.width,\n area.y.saturating_add(area.height).saturating_sub(list_y),\n );\n\n let summary = Paragraph::new(format_settings_summary(settings))\n .style(theme::text())\n .wrap(Wrap { trim: false });\n frame.render_widget(summary, summary_area);\n confirm_list.render(frame, list_area);\n}\n\nfn build_steps(_provider: &ProviderType) -> Vec<ConfigStep> {\n let mut steps = vec![ConfigStep::Provider];\n steps.extend_from_slice(&[\n ConfigStep::BaseUrl,\n ConfigStep::ApiKey,\n ConfigStep::Model,\n ConfigStep::Soul,\n ConfigStep::Language,\n ConfigStep::Confirm,\n ]);\n steps\n}\n\nfn step_display_name(step: ConfigStep) -> String {\n match step {\n ConfigStep::Provider => t!(\"init.step.provider\").to_string(),\n ConfigStep::BaseUrl => t!(\"init.step.base_url\").to_string(),\n ConfigStep::ApiKey => t!(\"init.step.api_key\").to_string(),\n ConfigStep::Model => t!(\"init.step.model\").to_string(),\n ConfigStep::Soul => t!(\"init.step.soul\").to_string(),\n ConfigStep::Language => t!(\"init.step.language\").to_string(),\n ConfigStep::Confirm => t!(\"common.save\").to_string(),\n }\n}\n\nfn current_value_for_step(step: ConfigStep, settings: &Settings) -> Option<String> {\n let value = match step {\n ConfigStep::Provider => format!(\"{:?}\", settings.llm.provider),\n ConfigStep::BaseUrl => settings.effective_base_url(),\n ConfigStep::ApiKey => {\n if let Some(key) = settings.llm.api_key.as_deref() {\n key.to_string()\n } else {\n t!(\"common.not_set\").to_string()\n }\n }\n ConfigStep::Model => settings.llm.model.clone(),\n ConfigStep::Soul => current_soul(settings),\n ConfigStep::Language => locale_display(&settings.ui.locale),\n ConfigStep::Confirm => return None,\n };\n Some(format!(\"{}: {}\", t!(\"common.current_values\"), value))\n}\n\nfn format_settings_summary(settings: &Settings) -> String {\n let api_key_status = if let Some(key) = settings.llm.api_key.as_deref() {\n key.to_string()\n } else {\n t!(\"common.not_set\").to_string()\n };\n let provider = format!(\"{:?}\", settings.llm.provider);\n\n format!(\n \"{}\\n\\n\\\n {} {}\\n\\\n {} {}\\n\\\n {} {}\\n\\\n {} {}\\n\\\n {} {}\\n\\\n {} {}\\n\\\n {} {}\",\n t!(\"common.current_values\"),\n t!(\"config.show_provider\"),\n provider,\n t!(\"init.step.mode\"),\n mode_display(settings),\n t!(\"config.show_base_url\"),\n settings.effective_base_url(),\n t!(\"config.show_api_key\"),\n api_key_status,\n t!(\"config.show_model\"),\n settings.llm.model,\n t!(\"config.show_soul\"),\n current_soul(settings),\n t!(\"config.show_language\"),\n locale_display(&settings.ui.locale),\n )\n}\n\nfn provider_list_for_settings(settings: &Settings) -> SelectableList<String> {\n let current = provider_key(&settings.llm.provider);\n let mut list = SelectableList::new(\n t!(\"config.select_provider\").to_string(),\n PROVIDERS\n .iter()\n .map(|provider| provider.to_string())\n .collect(),\n );\n if let Some(idx) = PROVIDERS.iter().position(|provider| *provider == current) {\n select_index(&mut list, idx);\n }\n list\n}\n\nfn base_url_list_for_settings(settings: &Settings) -> SelectableList<BaseUrlChoice> {\n let choices = base_url_choices(&settings.llm.provider, &settings.llm.provider_mode);\n let selected_idx = settings\n .llm\n .base_url\n .as_deref()\n .and_then(|url| {\n choices.iter().position(|choice| {\n choice\n .url\n .map(|known_url| known_url.trim() == url.trim())\n .unwrap_or(false)\n })\n })\n .unwrap_or_else(|| {\n if settings.llm.base_url.is_some() {\n choices\n .iter()\n .position(|choice| choice.url.is_none())\n .unwrap_or(0)\n } else {\n 0\n }\n });\n\n let mut list = SelectableList::new(t!(\"config.select_base_url\").to_string(), choices);\n select_index(&mut list, selected_idx);\n list\n}\n\nfn base_url_input_for_settings(settings: &Settings) -> TextInput {\n let input = TextInput::new(t!(\"config.base_url_label\").to_string());\n if let Some(base_url) = editable_custom_base_url(settings) {\n input.with_default(base_url)\n } else {\n input.with_placeholder(&settings.effective_base_url())\n }\n}\n\nfn editable_custom_base_url(settings: &Settings) -> Option<&str> {\n let base_url = settings.llm.base_url.as_deref()?.trim();\n if base_url.is_empty()\n || is_builtin_base_url(\n base_url,\n &settings.llm.provider,\n &settings.llm.provider_mode,\n )\n {\n None\n } else {\n Some(base_url)\n }\n}\n\nfn is_builtin_base_url(\n url: &str,\n provider: &ProviderType,\n provider_mode: &Option<ProviderMode>,\n) -> bool {\n base_url_choices(provider, provider_mode)\n .iter()\n .filter_map(|choice| choice.url)\n .any(|known_url| known_url.trim() == url.trim())\n}\n\nfn base_url_choices(\n provider: &ProviderType,\n _provider_mode: &Option<ProviderMode>,\n) -> Vec<BaseUrlChoice> {\n match provider {\n ProviderType::BigModel => vec![\n BaseUrlChoice {\n label: \"OpenAI API\",\n url: Some(BIGMODEL_OPENAI_BASE_URL),\n },\n BaseUrlChoice {\n label: \"Anthropic API\",\n url: Some(BIGMODEL_ANTHROPIC_BASE_URL),\n },\n BaseUrlChoice {\n label: \"Custom\",\n url: None,\n },\n ],\n ProviderType::XiaomiMimo => vec![\n BaseUrlChoice {\n label: \"OpenAI API\",\n url: Some(XIAOMI_MIMO_OPENAI_BASE_URL),\n },\n BaseUrlChoice {\n label: \"Anthropic API\",\n url: Some(XIAOMI_MIMO_ANTHROPIC_BASE_URL),\n },\n BaseUrlChoice {\n label: \"Custom\",\n url: None,\n },\n ],\n ProviderType::DeepSeek => vec![\n BaseUrlChoice {\n label: \"OpenAI API\",\n url: Some(DEEPSEEK_OPENAI_BASE_URL),\n },\n BaseUrlChoice {\n label: \"Anthropic API\",\n url: Some(DEEPSEEK_ANTHROPIC_BASE_URL),\n },\n BaseUrlChoice {\n label: \"Custom\",\n url: None,\n },\n ],\n ProviderType::Custom => vec![BaseUrlChoice {\n label: \"Custom\",\n url: None,\n }],\n _ => vec![BaseUrlChoice {\n label: \"Custom\",\n url: None,\n }],\n }\n}\n\nfn soul_list_for_settings(\n settings: &Settings,\n formulas: &[FormulaDef],\n) -> SelectableList<SoulChoice> {\n let current = current_soul(settings);\n let mut choices = vec![SoulChoice::KeepCurrent(current)];\n choices.extend(formulas.iter().cloned().map(SoulChoice::Formula));\n\n SelectableList::new(t!(\"config.select_soul\").to_string(), choices)\n}\n\nfn language_list_for_settings(settings: &Settings) -> SelectableList<String> {\n let locale_items: Vec<String> = LOCALES\n .iter()\n .map(|(code, name)| format!(\"{} - {}\", name, code))\n .collect();\n let mut list = SelectableList::new(t!(\"config.select_language\").to_string(), locale_items);\n if let Some(idx) = LOCALES\n .iter()\n .position(|(code, _)| *code == settings.ui.locale)\n {\n select_index(&mut list, idx);\n }\n list\n}\n\nfn build_confirm_list() -> SelectableList<ConfirmChoice> {\n SelectableList::new(\n t!(\"config.confirm_title\").to_string(),\n vec![ConfirmChoice::Save, ConfirmChoice::Cancel],\n )\n}\n\nfn parse_provider(provider: &str) -> ProviderType {\n match provider {\n \"bigmodel\" => ProviderType::BigModel,\n \"xiaomi_mimo\" => ProviderType::XiaomiMimo,\n \"deepseek\" => ProviderType::DeepSeek,\n _ => ProviderType::BigModel,\n }\n}\n\nfn provider_key(provider: &ProviderType) -> &'static str {\n match provider {\n ProviderType::BigModel => \"bigmodel\",\n ProviderType::XiaomiMimo => \"xiaomi_mimo\",\n ProviderType::DeepSeek => \"deepseek\",\n _ => \"bigmodel\",\n }\n}\n\nfn provider_mode_for_base_url(base_url: &str) -> Option<ProviderMode> {\n if base_url.contains(\"/anthropic\") {\n Some(ProviderMode::Native)\n } else {\n Some(ProviderMode::OpenAICompatible)\n }\n}\n\nfn locale_from_display(display: &str) -> String {\n LOCALES\n .iter()\n .find_map(|(code, name)| {\n if display.starts_with(name) {\n Some(code.to_string())\n } else {\n None\n }\n })\n .unwrap_or_else(|| \"en\".to_string())\n}\n\nfn locale_display(locale: &str) -> String {\n LOCALES\n .iter()\n .find(|(code, _)| *code == locale)\n .map(|(_, name)| name.to_string())\n .unwrap_or_else(|| locale.to_string())\n}\n\nfn current_soul(settings: &Settings) -> String {\n crate::formula::current_project_soul().unwrap_or_else(|| settings.soul.role.to_string())\n}\n\nfn mode_display(settings: &Settings) -> String {\n match settings.llm.provider_mode {\n Some(ProviderMode::Native) => \"Anthropic API\".to_string(),\n Some(ProviderMode::OpenAICompatible) => \"OpenAI API\".to_string(),\n _ if settings.effective_base_url().contains(\"/anthropic\") => \"Anthropic API\".to_string(),\n _ => \"OpenAI API\".to_string(),\n }\n}\n\nfn go_back(step_idx: &mut usize) -> bool {\n if *step_idx > 0 {\n *step_idx -= 1;\n true\n } else {\n false\n }\n}\n\nfn select_index<T: Clone + std::fmt::Display>(list: &mut SelectableList<T>, idx: usize) {\n for _ in 0..idx {\n let _ = list.handle_key(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n fn test_settings() -> Settings {\n Settings::default()\n }\n\n #[test]\n fn test_build_steps_openai() {\n let steps = build_steps(&ProviderType::Openai);\n assert_eq!(\n steps,\n vec![\n ConfigStep::Provider,\n ConfigStep::BaseUrl,\n ConfigStep::ApiKey,\n ConfigStep::Model,\n ConfigStep::Soul,\n ConfigStep::Language,\n ConfigStep::Confirm,\n ]\n );\n }\n\n #[test]\n fn test_build_steps_bigmodel_includes_mode() {\n let steps = build_steps(&ProviderType::BigModel);\n assert_eq!(steps[0], ConfigStep::Provider);\n assert_eq!(steps[1], ConfigStep::BaseUrl);\n assert_eq!(steps.last(), Some(&ConfigStep::Confirm));\n }\n\n #[test]\n fn test_provider_options() {\n assert_eq!(PROVIDERS.len(), 3);\n assert!(PROVIDERS.contains(&\"bigmodel\"));\n assert!(PROVIDERS.contains(&\"xiaomi_mimo\"));\n assert!(PROVIDERS.contains(&\"deepseek\"));\n }\n\n #[test]\n fn test_provider_parse() {\n let p = parse_provider(\"bigmodel\");\n assert!(matches!(p, ProviderType::BigModel));\n }\n\n #[test]\n fn test_bigmodel_base_url_options() {\n let choices = base_url_choices(&ProviderType::BigModel, &None);\n assert_eq!(choices.len(), 3);\n assert_eq!(\n choices[0].url,\n Some(\"https://open.bigmodel.cn/api/coding/paas/v4\")\n );\n assert_eq!(\n choices[1].url,\n Some(\"https://open.bigmodel.cn/api/anthropic\")\n );\n assert_eq!(choices[2].url, None);\n }\n\n #[test]\n fn test_deepseek_base_url_options() {\n let choices = base_url_choices(&ProviderType::DeepSeek, &None);\n assert_eq!(choices.len(), 3);\n assert_eq!(choices[0].url, Some(\"https://api.deepseek.com\"));\n assert_eq!(choices[1].url, Some(\"https://api.deepseek.com/anthropic\"));\n }\n\n #[test]\n fn test_base_url_input_starts_empty_for_builtin_default() {\n let mut settings = test_settings();\n settings.llm.provider = ProviderType::BigModel;\n settings.llm.base_url = Some(BIGMODEL_OPENAI_BASE_URL.to_string());\n\n let input = base_url_input_for_settings(&settings);\n\n assert_eq!(input.value(), \"\");\n }\n\n #[test]\n fn test_base_url_input_preserves_existing_custom_url() {\n let mut settings = test_settings();\n settings.llm.provider = ProviderType::BigModel;\n settings.llm.base_url = Some(\"http://localhost:11434/v1\".to_string());\n\n let input = base_url_input_for_settings(&settings);\n\n assert_eq!(input.value(), \"http://localhost:11434/v1\");\n }\n\n #[test]\n fn test_settings_default_values() {\n let s = test_settings();\n assert_eq!(s.llm.model, \"gpt-4o-mini\");\n assert!(s.llm.api_key.is_none());\n }\n\n #[test]\n fn test_soul_choices_keep_current() {\n let settings = test_settings();\n let list = soul_list_for_settings(&settings, &[]);\n\n assert!(matches!(list.selected(), Some(SoulChoice::KeepCurrent(_))));\n }\n\n #[test]\n fn test_language_parse() {\n assert_eq!(locale_from_display(\"中文 (简体) - zh-CN\"), \"zh-CN\");\n assert_eq!(locale_from_display(\"English - en\"), \"en\");\n }\n}\n"}}, ".github/workflows/release.yml": {"3d56e304e096c6e5745d05444eeed4a5d591db55": {"t": 1785746344.988857, "n": 25855, "h": "05a01b78875ffcff", "c": "name: Release\n\non:\n push:\n branches:\n - release\n\npermissions:\n contents: write\n id-token: write\n\nconcurrency:\n group: release-${{ github.ref }}\n cancel-in-progress: false\n\nenv:\n CARGO_TERM_COLOR: always\n CARGO_INCREMENTAL: \"0\"\n\njobs:\n resolve-version:\n name: Resolve Version\n runs-on: ubuntu-latest\n outputs:\n version: ${{ steps.version.outputs.version }}\n tag_name: ${{ steps.version.outputs.tag_name }}\n steps:\n - uses: actions/checkout@v6\n with:\n fetch-depth: 0\n token: ${{ secrets.ALIUS_RELEASE_TOKEN }}\n\n - name: Read version from VERSION.txt\n id: version\n run: |\n VERSION=$(cat VERSION.txt | tr -d '[:space:]')\n echo \"version=${VERSION}\" >> $GITHUB_OUTPUT\n echo \"tag_name=v${VERSION}\" >> $GITHUB_OUTPUT\n\n - name: Check version consistency\n run: bash scripts/check-version.sh\n\n test-gate:\n name: Release Test Gate\n needs: resolve-version\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v6\n\n - name: Install Rust\n uses: dtolnay/rust-toolchain@stable\n with:\n components: rustfmt, clippy, llvm-tools-preview\n\n - name: Install cargo-llvm-cov\n run: cargo install cargo-llvm-cov --locked\n\n - name: Cache Rust artifacts\n uses: Swatinem/rust-cache@v2\n with:\n key: release-test-gate\n\n - name: Check formatting\n run: cargo fmt --all -- --check\n\n - name: Run clippy\n run: cargo clippy --locked --workspace --all-targets --features testing -- -D warnings\n\n - name: Run tests with coverage\n run: |\n mkdir -p target/ci-reports target/llvm-cov\n cargo llvm-cov --locked --workspace --features testing \\\n --no-report -- --test-threads=1 --nocapture 2>&1 | tee target/ci-reports/test.log\n # Parse test result lines\n grep 'test result:' target/ci-reports/test.log > target/ci-reports/test-result-lines.txt || true\n # Write summary env\n PASSED=$(grep -oP '(\\d+) passed' target/ci-reports/test-result-lines.txt | tail -1 | grep -oP '\\d+' || echo 0)\n FAILED=$(grep -oP '(\\d+) failed' target/ci-reports/test-result-lines.txt | tail -1 | grep -oP '\\d+' || echo 0)\n echo \"tests_passed=${PASSED}\" > target/ci-reports/test-summary.env\n echo \"tests_failed=${FAILED}\" >> target/ci-reports/test-summary.env\n\n - name: Generate coverage reports\n run: |\n COV_IGNORE='--ignore-filename-regex (/tests/|/testing\\.rs$|/testkit/|state_machine_tests\\.rs$)'\n cargo llvm-cov report --lcov $COV_IGNORE --output-path target/llvm-cov/lcov.info\n cargo llvm-cov report --html $COV_IGNORE --output-dir target/llvm-cov/html\n # Extract TOTAL line for compact job summary\n cargo llvm-cov report $COV_IGNORE | grep \"^TOTAL\" | tee target/llvm-cov/summary.txt\n\n - name: Enforce coverage threshold\n run: |\n # Staged gate: current baseline ~67.5%. See docs/standards/validation.md §Staged Coverage Threshold.\n cargo llvm-cov report --ignore-filename-regex '(/tests/|/testing\\.rs$|/testkit/|state_machine_tests\\.rs$)' --fail-under-lines 65\n\n - name: Build release binary\n run: cargo build -p alius-cli --bin alius --release --locked\n\n - name: Scan release binary for test symbols\n run: |\n SCAN=$(strings target/release/alius | grep -cE 'FakeProvider|FakeTool|TuiTestHarness|CoreRuntimeHarness|VecEventSource|testing::|testkit' || true)\n if [ \"$SCAN\" -gt 0 ]; then\n echo \"ERROR: Test symbols found in release binary\"\n strings target/release/alius | grep -E 'FakeProvider|FakeTool|TuiTestHarness|CoreRuntimeHarness|VecEventSource|testing::|testkit'\n exit 1\n fi\n echo \"Release binary clean — no test symbols found\"\n\n - name: Install cargo-audit\n uses: taiki-e/install-action@v2\n with:\n tool: cargo-audit\n\n - name: Run security audit\n # Exceptions documented in docs/audit-exceptions.md\n run: cargo audit --deny warnings --ignore RUSTSEC-2025-0012 --ignore RUSTSEC-2024-0384 --ignore RUSTSEC-2024-0436 --ignore RUSTSEC-2026-0002\n\n - name: Write job summary\n if: always()\n run: |\n echo \"## Release Test Gate\" >> $GITHUB_STEP_SUMMARY\n echo \"\" >> $GITHUB_STEP_SUMMARY\n if [ -f target/ci-reports/test-result-lines.txt ]; then\n echo '```' >> $GITHUB_STEP_SUMMARY\n cat target/ci-reports/test-result-lines.txt >> $GITHUB_STEP_SUMMARY\n echo '```' >> $GITHUB_STEP_SUMMARY\n fi\n if [ -f target/llvm-cov/summary.txt ]; then\n echo \"\" >> $GITHUB_STEP_SUMMARY\n echo \"### Coverage\" >> $GITHUB_STEP_SUMMARY\n echo '```' >> $GITHUB_STEP_SUMMARY\n cat target/llvm-cov/summary.txt >> $GITHUB_STEP_SUMMARY\n echo '```' >> $GITHUB_STEP_SUMMARY\n fi\n\n - name: Upload test artifacts\n if: always()\n uses: actions/upload-artifact@v4\n with:\n name: release-test-gate-artifacts\n path: |\n target/ci-reports/\n target/llvm-cov/\n retention-days: 30\n\n provider-smoke-gate:\n name: Provider Smoke Gate\n needs: resolve-version\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v6\n\n - name: Install Rust\n uses: dtolnay/rust-toolchain@stable\n\n - name: Cache Rust artifacts\n uses: Swatinem/rust-cache@v2\n with:\n key: release-provider-smoke\n\n - name: Build alius binary\n run: cargo build -p alius-cli --bin alius --locked\n\n - name: Smoke: chat request\n env:\n DEEPSEEK_API_KEY: ${{ secrets.ALIUS_TEST_API_KEY }}\n ALIUS_TEST_MODEL: ${{ vars.ALIUS_TEST_MODEL || 'deepseek-chat' }}\n run: |\n if [ -z \"$DEEPSEEK_API_KEY\" ]; then\n echo \"::error::ALIUS_TEST_API_KEY secret is not set\"\n exit 1\n fi\n echo \"Provider: deepseek\"\n echo \"Model: $ALIUS_TEST_MODEL\"\n\n SMOKE_DIR=$(mktemp -d)\n mkdir -p \"$SMOKE_DIR/.alius/config\"\n\n cat > \"$SMOKE_DIR/.alius/config/config.toml\" <<TOML\n [project]\n name = \"smoke-test\"\n version = 1\n\n [runtime]\n default_mode = \"plan\"\n tui_workspace = false\n\n [model]\n default_provider = \"deepseek\"\n default_model = \"${ALIUS_TEST_MODEL}\"\n TOML\n\n cat > \"$SMOKE_DIR/.alius/config/providers.toml\" <<TOML\n [router]\n strategy = \"tier\"\n default_tier = \"medium\"\n fallback_tier = \"light\"\n\n [tiers.medium]\n provider = \"deepseek\"\n model = \"${ALIUS_TEST_MODEL}\"\n\n [tiers.light]\n provider = \"deepseek\"\n model = \"${ALIUS_TEST_MODEL}\"\n\n [providers.deepseek]\n enabled = true\n kind = \"deepseek\"\n base_url = \"https://api.deepseek.com\"\n api_key_env = \"DEEPSEEK_API_KEY\"\n TOML\n\n cd \"$SMOKE_DIR\"\n timeout 60 ../target/release/alius run \\\n --provider deepseek \\\n --model \"$ALIUS_TEST_MODEL\" \\\n -p \"Reply with exactly: OK\" 2>&1 | tee /tmp/provider-smoke.log\n\n if grep -qE 'Run failed|Error:|internal protocol error|model client' /tmp/provider-smoke.log; then\n echo \"::error::Provider smoke test failed\"\n cat /tmp/provider-smoke.log\n exit 1\n fi\n\n if ! grep -qE '[a-zA-Z]{2}' /tmp/provider-smoke.log; then\n echo \"::error::Provider smoke test produced no output\"\n cat /tmp/provider-smoke.log\n exit 1\n fi\n\n echo \"Provider smoke test passed\"\n\n - name: Write job summary\n if: always()\n run: |\n echo \"## Provider Smoke Gate\" >> $GITHUB_STEP_SUMMARY\n echo \"\" >> $GITHUB_STEP_SUMMARY\n if [ -f /tmp/provider-smoke.log ]; then\n echo '```' >> $GITHUB_STEP_SUMMARY\n head -20 /tmp/provider-smoke.log >> $GITHUB_STEP_SUMMARY\n echo '```' >> $GITHUB_STEP_SUMMARY\n fi\n\n create-release:\n name: Create GitHub Release\n needs: [resolve-version, test-gate, provider-smoke-gate]\n runs-on: ubuntu-latest\n permissions:\n contents: write\n steps:\n - uses: actions/checkout@v6\n with:\n fetch-depth: 0\n token: ${{ secrets.ALIUS_RELEASE_TOKEN }}\n\n - name: Create tag (after test-gate passed)\n run: |\n TAG=\"${{ needs.resolve-version.outputs.tag_name }}\"\n if git ls-remote origin \"refs/tags/${TAG}\" | grep -q .; then\n echo \"Tag ${TAG} already exists\"\n else\n git tag \"$TAG\"\n git push origin \"$TAG\"\n echo \"Created tag ${TAG}\"\n fi\n\n - name: Create or verify release\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n TAG=\"${{ needs.resolve-version.outputs.tag_name }}\"\n VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n\n if gh release view \"$TAG\" >/dev/null 2>&1; then\n echo \"Release $TAG already exists, skipping creation\"\n else\n TAG_SHA=$(git ls-remote origin \"refs/tags/${TAG}\" | cut -f1)\n if [ -z \"$TAG_SHA\" ]; then\n TAG_SHA=\"$GITHUB_SHA\"\n fi\n echo \"Creating release $TAG at commit $TAG_SHA...\"\n gh release create \"$TAG\" \\\n --title \"v${VERSION}\" \\\n --generate-notes \\\n --target \"$TAG_SHA\"\n fi\n\n build-linux:\n name: Build Linux\n needs: [resolve-version, create-release]\n runs-on: ubuntu-latest\n permissions:\n contents: write\n steps:\n - uses: actions/checkout@v6\n\n - name: Install Rust\n uses: dtolnay/rust-toolchain@stable\n\n - name: Verify and fix version consistency\n run: |\n TAG_VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n CARGO_VERSION=$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[] | select(.name==\"alius-cli\") | .version')\n echo \"Tag version: $TAG_VERSION\"\n echo \"Cargo version: $CARGO_VERSION\"\n if [ \"$TAG_VERSION\" != \"$CARGO_VERSION\" ]; then\n echo \"::warning::Tag version $TAG_VERSION != Cargo version $CARGO_VERSION, auto-fixing Cargo.toml\"\n sed -i \"s/^version = \\\".*\\\"/version = \\\"${TAG_VERSION}\\\"/\" Cargo.toml\n echo \"Cargo.toml updated to version $TAG_VERSION\"\n fi\n\n - name: Cache Rust artifacts\n uses: Swatinem/rust-cache@v2\n with:\n key: ${{ needs.resolve-version.outputs.version }}-x86_64-linux\n workspaces: \". -> target\"\n cache-on-failure: true\n\n - name: Build release binary\n run: cargo build -p alius-cli --bin alius --release --locked\n\n - name: Scan release binary for test symbols\n run: |\n SCAN=$(strings target/release/alius | grep -cE 'FakeProvider|FakeTool|TuiTestHarness|CoreRuntimeHarness|VecEventSource|testing::|testkit' || true)\n if [ \"$SCAN\" -gt 0 ]; then\n echo \"ERROR: Test symbols found in release binary\"\n exit 1\n fi\n\n - name: Prepare artifacts\n run: |\n cd target/release\n tar -czvf ../../alius-linux-x64.tar.gz alius\n cd ../..\n\n - name: Upload artifact to release\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n gh release upload ${{ needs.resolve-version.outputs.tag_name }} \\\n alius-linux-x64.tar.gz --clobber\n\n build-macos-x64:\n name: Build macOS x64\n needs: [resolve-version, create-release]\n runs-on: macos-26-intel\n steps:\n - uses: actions/checkout@v6\n\n - name: Verify and fix version consistency\n run: |\n TAG_VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n CARGO_VERSION=$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[] | select(.name==\"alius-cli\") | .version')\n echo \"Tag version: $TAG_VERSION\"\n echo \"Cargo version: $CARGO_VERSION\"\n if [ \"$TAG_VERSION\" != \"$CARGO_VERSION\" ]; then\n echo \"::warning::Tag version $TAG_VERSION != Cargo version $CARGO_VERSION, auto-fixing Cargo.toml\"\n sed -i '' \"s/^version = \\\".*\\\"/version = \\\"${TAG_VERSION}\\\"/\" Cargo.toml\n echo \"Cargo.toml updated to version $TAG_VERSION\"\n fi\n\n - name: Install Rust\n uses: dtolnay/rust-toolchain@stable\n\n - name: Setup sccache\n uses: mozilla-actions/sccache-action@v0.0.9\n\n - name: Cache Rust artifacts\n uses: Swatinem/rust-cache@v2\n with:\n key: ${{ needs.resolve-version.outputs.version }}-x86_64-darwin\n cache-on-failure: true\n\n - name: Build release binary\n run: cargo build -p alius-cli --bin alius --release --locked\n\n - name: Scan release binary for test symbols\n run: |\n SCAN=$(strings target/release/alius | grep -cE 'FakeProvider|FakeTool|TuiTestHarness|CoreRuntimeHarness|VecEventSource|testing::|testkit' || true)\n if [ \"$SCAN\" -gt 0 ]; then\n echo \"ERROR: Test symbols found in release binary\"\n exit 1\n fi\n\n - name: Prepare artifact\n run: |\n cd target/release\n tar -czvf ../../alius-macos-x64.tar.gz alius\n cd ../..\n\n - name: Upload artifact to release\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n gh release upload ${{ needs.resolve-version.outputs.tag_name }} \\\n alius-macos-x64.tar.gz --clobber\n\n build-macos-arm64:\n name: Build macOS ARM64\n needs: [resolve-version, create-release]\n runs-on: macos-26\n steps:\n - uses: actions/checkout@v6\n\n - name: Verify and fix version consistency\n run: |\n TAG_VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n CARGO_VERSION=$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[] | select(.name==\"alius-cli\") | .version')\n echo \"Tag version: $TAG_VERSION\"\n echo \"Cargo version: $CARGO_VERSION\"\n if [ \"$TAG_VERSION\" != \"$CARGO_VERSION\" ]; then\n echo \"::warning::Tag version $TAG_VERSION != Cargo version $CARGO_VERSION, auto-fixing Cargo.toml\"\n sed -i '' \"s/^version = \\\".*\\\"/version = \\\"${TAG_VERSION}\\\"/\" Cargo.toml\n echo \"Cargo.toml updated to version $TAG_VERSION\"\n fi\n\n - name: Install Rust\n uses: dtolnay/rust-toolchain@stable\n\n - name: Setup sccache\n uses: mozilla-actions/sccache-action@v0.0.9\n\n - name: Cache Rust artifacts\n uses: Swatinem/rust-cache@v2\n with:\n key: ${{ needs.resolve-version.outputs.version }}-aarch64-darwin\n cache-on-failure: true\n\n - name: Build release binary\n run: cargo build -p alius-cli --bin alius --release --locked\n\n - name: Scan release binary for test symbols\n run: |\n SCAN=$(strings target/release/alius | grep -cE 'FakeProvider|FakeTool|TuiTestHarness|CoreRuntimeHarness|VecEventSource|testing::|testkit' || true)\n if [ \"$SCAN\" -gt 0 ]; then\n echo \"ERROR: Test symbols found in release binary\"\n exit 1\n fi\n\n - name: Prepare artifact\n run: |\n cd target/release\n tar -czvf ../../alius-macos-arm64.tar.gz alius\n cd ../..\n\n - name: Upload artifact to release\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n gh release upload ${{ needs.resolve-version.outputs.tag_name }} \\\n alius-macos-arm64.tar.gz --clobber\n\n build-windows:\n name: Build Windows\n needs: [resolve-version, create-release]\n runs-on: windows-2025-vs2026\n steps:\n - uses: actions/checkout@v6\n\n - name: Verify and fix version consistency\n shell: bash\n run: |\n TAG_VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n CARGO_VERSION=$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[] | select(.name==\"alius-cli\") | .version')\n echo \"Tag version: $TAG_VERSION\"\n echo \"Cargo version: $CARGO_VERSION\"\n if [ \"$TAG_VERSION\" != \"$CARGO_VERSION\" ]; then\n echo \"::warning::Tag version $TAG_VERSION != Cargo version $CARGO_VERSION, auto-fixing Cargo.toml\"\n sed -i \"s/^version = \\\".*\\\"/version = \\\"${TAG_VERSION}\\\"/\" Cargo.toml\n echo \"Cargo.toml updated to version $TAG_VERSION\"\n fi\n\n - name: Install Rust\n uses: dtolnay/rust-toolchain@stable\n\n - name: Setup sccache\n uses: mozilla-actions/sccache-action@v0.0.9\n\n - name: Cache Rust artifacts\n uses: Swatinem/rust-cache@v2\n with:\n key: ${{ needs.resolve-version.outputs.version }}-x86_64-windows\n cache-on-failure: true\n\n - name: Build release binary\n run: cargo build -p alius-cli --bin alius --release --locked\n\n - name: Scan release binary for test symbols\n shell: bash\n run: |\n SCAN=$(strings target/release/alius.exe | grep -cE 'FakeProvider|FakeTool|TuiTestHarness|CoreRuntimeHarness|VecEventSource|testing::|testkit' || true)\n if [ \"$SCAN\" -gt 0 ]; then\n echo \"ERROR: Test symbols found in release binary\"\n exit 1\n fi\n\n - name: Prepare artifact\n run: |\n cd target/release\n 7z a ../../alius-windows-x64.zip alius.exe\n cd ../..\n\n - name: Upload artifact to release\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n gh release upload ${{ needs.resolve-version.outputs.tag_name }} \\\n alius-windows-x64.zip --clobber\n\n verify-assets:\n name: Verify Release Assets\n needs:\n - resolve-version\n - build-linux\n - build-macos-x64\n - build-macos-arm64\n - build-windows\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v6\n\n - name: Verify all assets exist\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n TAG: ${{ needs.resolve-version.outputs.tag_name }}\n run: |\n echo \"Verifying assets for release $TAG...\"\n\n ASSETS=$(gh release view \"$TAG\" --json assets --jq '.assets[].name')\n echo \"Found assets:\"\n echo \"$ASSETS\"\n\n REQUIRED=(\n \"alius-linux-x64.tar.gz\"\n \"alius-macos-x64.tar.gz\"\n \"alius-macos-arm64.tar.gz\"\n \"alius-windows-x64.zip\"\n )\n\n MISSING=0\n for asset in \"${REQUIRED[@]}\"; do\n if ! echo \"$ASSETS\" | grep -q \"$asset\"; then\n echo \"::error::Missing required asset: $asset\"\n MISSING=1\n fi\n done\n\n if [ $MISSING -ne 0 ]; then\n echo \"::error::Some required assets are missing\"\n exit 1\n fi\n\n echo \"All required assets verified\"\n\n publish-npm:\n name: Publish npm\n needs:\n - resolve-version\n - verify-assets\n runs-on: ubuntu-latest\n permissions:\n contents: read\n id-token: write\n steps:\n - uses: actions/checkout@v6\n\n - name: Setup Node.js\n uses: actions/setup-node@v6\n with:\n node-version: '24'\n registry-url: 'https://registry.npmjs.org'\n package-manager-cache: false\n\n - name: Verify Trusted Publishing prerequisites\n run: |\n node --version\n npm --version\n node - <<'NODE'\n const { execSync } = require('child_process');\n\n function assertAtLeast(name, actual, minimum) {\n const got = actual.replace(/^v/, '').split('.').map(Number);\n const min = minimum.split('.').map(Number);\n for (let i = 0; i < min.length; i += 1) {\n if ((got[i] || 0) > min[i]) return;\n if ((got[i] || 0) < min[i]) {\n throw new Error(`${name} ${actual} is below required ${minimum}`);\n }\n }\n }\n\n assertAtLeast('Node.js', process.version, '22.14.0');\n assertAtLeast('npm', execSync('npm --version', { encoding: 'utf8' }).trim(), '11.5.1');\n NODE\n\n - name: Prepare npm packages\n run: |\n VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n RELEASE_TAG=\"${{ needs.resolve-version.outputs.tag_name }}\"\n node npm-packages/generate-packages.js \"$VERSION\" \"$RELEASE_TAG\"\n node npm-packages/verify-release-version.js \"$VERSION\" \"$RELEASE_TAG\"\n\n - name: Publish platform packages\n run: |\n VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n\n for pkg in alius-darwin-x64 alius-darwin-arm64 alius-linux-x64 alius-win32-x64; do\n if npm view \"@alius-tech/$pkg@$VERSION\" version >/dev/null 2>&1; then\n echo \"Skipping @alius-tech/$pkg@$VERSION (already published)\"\n continue\n fi\n echo \"Publishing @alius-tech/$pkg@$VERSION...\"\n cd \"npm-packages/$pkg\"\n npm publish --access public --provenance --tag latest\n cd ../..\n done\n\n - name: Publish main package\n run: |\n VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n\n if npm view \"@alius-tech/alius@$VERSION\" version >/dev/null 2>&1; then\n echo \"Skipping @alius-tech/alius@$VERSION (already published)\"\n exit 0\n fi\n\n cd npm-packages/alius\n npm publish --access public --provenance --tag latest\n\n update-homebrew:\n name: Update Homebrew\n needs:\n - resolve-version\n - verify-assets\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v6\n\n - name: Download release artifacts\n run: |\n RELEASE_TAG=\"${{ needs.resolve-version.outputs.tag_name }}\"\n BASE_URL=\"https://github.com/AliusTech/alius/releases/download/${RELEASE_TAG}\"\n mkdir -p artifacts\n\n curl -fsSL -o artifacts/alius-macos-arm64.tar.gz \"${BASE_URL}/alius-macos-arm64.tar.gz\"\n curl -fsSL -o artifacts/alius-macos-x64.tar.gz \"${BASE_URL}/alius-macos-x64.tar.gz\"\n curl -fsSL -o artifacts/alius-linux-x64.tar.gz \"${BASE_URL}/alius-linux-x64.tar.gz\"\n\n - name: Compute SHA256 checksums\n id: sha256\n run: |\n echo \"arm64=$(sha256sum artifacts/alius-macos-arm64.tar.gz | cut -d' ' -f1)\" >> $GITHUB_OUTPUT\n echo \"x64=$(sha256sum artifacts/alius-macos-x64.tar.gz | cut -d' ' -f1)\" >> $GITHUB_OUTPUT\n echo \"linux_x64=$(sha256sum artifacts/alius-linux-x64.tar.gz | cut -d' ' -f1)\" >> $GITHUB_OUTPUT\n\n - name: Checkout homebrew tap\n uses: actions/checkout@v6\n with:\n repository: AliusTech/homebrew-tap\n token: ${{ secrets.TAP_GITHUB_TOKEN }}\n path: tap\n\n - name: Check if version already published\n id: check\n run: |\n VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n FORMULA=\"tap/Formula/alius.rb\"\n if [ -f \"$FORMULA\" ] && grep -q \"version \\\"${VERSION}\\\"\" \"$FORMULA\"; then\n echo \"Homebrew formula already at version ${VERSION}, skipping\"\n echo \"skip=true\" >> $GITHUB_OUTPUT\n else\n echo \"skip=false\" >> $GITHUB_OUTPUT\n fi\n\n - name: Update formula\n if: steps.check.outputs.skip != 'true'\n env:\n VERSION: ${{ needs.resolve-version.outputs.version }}\n SHA256_ARM64: ${{ steps.sha256.outputs.arm64 }}\n SHA256_X64: ${{ steps.sha256.outputs.x64 }}\n SHA256_LINUX_X64: ${{ steps.sha256.outputs.linux_x64 }}\n run: |\n rm -rf tap/Formula\n mkdir -p tap/Formula\n cat > tap/Formula/alius.rb <<'TEMPLATE'\n class Alius < Formula\n desc \"LLM Agent CLI - Software Self-Evolution\"\n homepage \"https://github.com/AliusTech/alius\"\n version \"%%VERSION%%\"\n license \"MIT\"\n\n on_macos do\n if Hardware::CPU.arm?\n url \"https://github.com/AliusTech/alius/releases/download/%%RELEASE_TAG%%/alius-macos-arm64.tar.gz\"\n sha256 \"%%SHA256_ARM64%%\"\n else\n url \"https://github.com/AliusTech/alius/releases/download/%%RELEASE_TAG%%/alius-macos-x64.tar.gz\"\n sha256 \"%%SHA256_X64%%\"\n end\n end\n\n on_linux do\n url \"https://github.com/AliusTech/alius/releases/download/%%RELEASE_TAG%%/alius-linux-x64.tar.gz\"\n sha256 \"%%SHA256_LINUX_X64%%\"\n end\n\n def install\n bin.install \"alius\"\n end\n\n test do\n system bin/\"alius\", \"--version\"\n end\n end\n TEMPLATE\n\n sed -i \"s/%%VERSION%%/${VERSION}/g\" tap/Formula/alius.rb\n sed -i \"s/%%RELEASE_TAG%%/${{ needs.resolve-version.outputs.tag_name }}/g\" tap/Formula/alius.rb\n sed -i \"s/%%SHA256_ARM64%%/${SHA256_ARM64}/g\" tap/Formula/alius.rb\n sed -i \"s/%%SHA256_X64%%/${SHA256_X64}/g\" tap/Formula/alius.rb\n sed -i \"s/%%SHA256_LINUX_X64%%/${SHA256_LINUX_X64}/g\" tap/Formula/alius.rb\n\n - name: Push formula update\n if: steps.check.outputs.skip != 'true'\n run: |\n VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n cd tap\n git config user.name \"github-actions[bot]\"\n git config user.email \"github-actions[bot]@users.noreply.github.com\"\n git add Formula/alius.rb\n git diff --staged --quiet && exit 0\n git commit -m \"Update alius to ${VERSION}\"\n git push\n"}, "33f083f4930e7efa8ca44a8cfbd2bb5dc78a503a": {"t": 1785750730.6281967, "n": 25855, "h": "05a01b78875ffcff", "c": "name: Release\n\non:\n push:\n branches:\n - release\n\npermissions:\n contents: write\n id-token: write\n\nconcurrency:\n group: release-${{ github.ref }}\n cancel-in-progress: false\n\nenv:\n CARGO_TERM_COLOR: always\n CARGO_INCREMENTAL: \"0\"\n\njobs:\n resolve-version:\n name: Resolve Version\n runs-on: ubuntu-latest\n outputs:\n version: ${{ steps.version.outputs.version }}\n tag_name: ${{ steps.version.outputs.tag_name }}\n steps:\n - uses: actions/checkout@v6\n with:\n fetch-depth: 0\n token: ${{ secrets.ALIUS_RELEASE_TOKEN }}\n\n - name: Read version from VERSION.txt\n id: version\n run: |\n VERSION=$(cat VERSION.txt | tr -d '[:space:]')\n echo \"version=${VERSION}\" >> $GITHUB_OUTPUT\n echo \"tag_name=v${VERSION}\" >> $GITHUB_OUTPUT\n\n - name: Check version consistency\n run: bash scripts/check-version.sh\n\n test-gate:\n name: Release Test Gate\n needs: resolve-version\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v6\n\n - name: Install Rust\n uses: dtolnay/rust-toolchain@stable\n with:\n components: rustfmt, clippy, llvm-tools-preview\n\n - name: Install cargo-llvm-cov\n run: cargo install cargo-llvm-cov --locked\n\n - name: Cache Rust artifacts\n uses: Swatinem/rust-cache@v2\n with:\n key: release-test-gate\n\n - name: Check formatting\n run: cargo fmt --all -- --check\n\n - name: Run clippy\n run: cargo clippy --locked --workspace --all-targets --features testing -- -D warnings\n\n - name: Run tests with coverage\n run: |\n mkdir -p target/ci-reports target/llvm-cov\n cargo llvm-cov --locked --workspace --features testing \\\n --no-report -- --test-threads=1 --nocapture 2>&1 | tee target/ci-reports/test.log\n # Parse test result lines\n grep 'test result:' target/ci-reports/test.log > target/ci-reports/test-result-lines.txt || true\n # Write summary env\n PASSED=$(grep -oP '(\\d+) passed' target/ci-reports/test-result-lines.txt | tail -1 | grep -oP '\\d+' || echo 0)\n FAILED=$(grep -oP '(\\d+) failed' target/ci-reports/test-result-lines.txt | tail -1 | grep -oP '\\d+' || echo 0)\n echo \"tests_passed=${PASSED}\" > target/ci-reports/test-summary.env\n echo \"tests_failed=${FAILED}\" >> target/ci-reports/test-summary.env\n\n - name: Generate coverage reports\n run: |\n COV_IGNORE='--ignore-filename-regex (/tests/|/testing\\.rs$|/testkit/|state_machine_tests\\.rs$)'\n cargo llvm-cov report --lcov $COV_IGNORE --output-path target/llvm-cov/lcov.info\n cargo llvm-cov report --html $COV_IGNORE --output-dir target/llvm-cov/html\n # Extract TOTAL line for compact job summary\n cargo llvm-cov report $COV_IGNORE | grep \"^TOTAL\" | tee target/llvm-cov/summary.txt\n\n - name: Enforce coverage threshold\n run: |\n # Staged gate: current baseline ~67.5%. See docs/standards/validation.md §Staged Coverage Threshold.\n cargo llvm-cov report --ignore-filename-regex '(/tests/|/testing\\.rs$|/testkit/|state_machine_tests\\.rs$)' --fail-under-lines 65\n\n - name: Build release binary\n run: cargo build -p alius-cli --bin alius --release --locked\n\n - name: Scan release binary for test symbols\n run: |\n SCAN=$(strings target/release/alius | grep -cE 'FakeProvider|FakeTool|TuiTestHarness|CoreRuntimeHarness|VecEventSource|testing::|testkit' || true)\n if [ \"$SCAN\" -gt 0 ]; then\n echo \"ERROR: Test symbols found in release binary\"\n strings target/release/alius | grep -E 'FakeProvider|FakeTool|TuiTestHarness|CoreRuntimeHarness|VecEventSource|testing::|testkit'\n exit 1\n fi\n echo \"Release binary clean — no test symbols found\"\n\n - name: Install cargo-audit\n uses: taiki-e/install-action@v2\n with:\n tool: cargo-audit\n\n - name: Run security audit\n # Exceptions documented in docs/audit-exceptions.md\n run: cargo audit --deny warnings --ignore RUSTSEC-2025-0012 --ignore RUSTSEC-2024-0384 --ignore RUSTSEC-2024-0436 --ignore RUSTSEC-2026-0002\n\n - name: Write job summary\n if: always()\n run: |\n echo \"## Release Test Gate\" >> $GITHUB_STEP_SUMMARY\n echo \"\" >> $GITHUB_STEP_SUMMARY\n if [ -f target/ci-reports/test-result-lines.txt ]; then\n echo '```' >> $GITHUB_STEP_SUMMARY\n cat target/ci-reports/test-result-lines.txt >> $GITHUB_STEP_SUMMARY\n echo '```' >> $GITHUB_STEP_SUMMARY\n fi\n if [ -f target/llvm-cov/summary.txt ]; then\n echo \"\" >> $GITHUB_STEP_SUMMARY\n echo \"### Coverage\" >> $GITHUB_STEP_SUMMARY\n echo '```' >> $GITHUB_STEP_SUMMARY\n cat target/llvm-cov/summary.txt >> $GITHUB_STEP_SUMMARY\n echo '```' >> $GITHUB_STEP_SUMMARY\n fi\n\n - name: Upload test artifacts\n if: always()\n uses: actions/upload-artifact@v4\n with:\n name: release-test-gate-artifacts\n path: |\n target/ci-reports/\n target/llvm-cov/\n retention-days: 30\n\n provider-smoke-gate:\n name: Provider Smoke Gate\n needs: resolve-version\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v6\n\n - name: Install Rust\n uses: dtolnay/rust-toolchain@stable\n\n - name: Cache Rust artifacts\n uses: Swatinem/rust-cache@v2\n with:\n key: release-provider-smoke\n\n - name: Build alius binary\n run: cargo build -p alius-cli --bin alius --locked\n\n - name: Smoke: chat request\n env:\n DEEPSEEK_API_KEY: ${{ secrets.ALIUS_TEST_API_KEY }}\n ALIUS_TEST_MODEL: ${{ vars.ALIUS_TEST_MODEL || 'deepseek-chat' }}\n run: |\n if [ -z \"$DEEPSEEK_API_KEY\" ]; then\n echo \"::error::ALIUS_TEST_API_KEY secret is not set\"\n exit 1\n fi\n echo \"Provider: deepseek\"\n echo \"Model: $ALIUS_TEST_MODEL\"\n\n SMOKE_DIR=$(mktemp -d)\n mkdir -p \"$SMOKE_DIR/.alius/config\"\n\n cat > \"$SMOKE_DIR/.alius/config/config.toml\" <<TOML\n [project]\n name = \"smoke-test\"\n version = 1\n\n [runtime]\n default_mode = \"plan\"\n tui_workspace = false\n\n [model]\n default_provider = \"deepseek\"\n default_model = \"${ALIUS_TEST_MODEL}\"\n TOML\n\n cat > \"$SMOKE_DIR/.alius/config/providers.toml\" <<TOML\n [router]\n strategy = \"tier\"\n default_tier = \"medium\"\n fallback_tier = \"light\"\n\n [tiers.medium]\n provider = \"deepseek\"\n model = \"${ALIUS_TEST_MODEL}\"\n\n [tiers.light]\n provider = \"deepseek\"\n model = \"${ALIUS_TEST_MODEL}\"\n\n [providers.deepseek]\n enabled = true\n kind = \"deepseek\"\n base_url = \"https://api.deepseek.com\"\n api_key_env = \"DEEPSEEK_API_KEY\"\n TOML\n\n cd \"$SMOKE_DIR\"\n timeout 60 ../target/release/alius run \\\n --provider deepseek \\\n --model \"$ALIUS_TEST_MODEL\" \\\n -p \"Reply with exactly: OK\" 2>&1 | tee /tmp/provider-smoke.log\n\n if grep -qE 'Run failed|Error:|internal protocol error|model client' /tmp/provider-smoke.log; then\n echo \"::error::Provider smoke test failed\"\n cat /tmp/provider-smoke.log\n exit 1\n fi\n\n if ! grep -qE '[a-zA-Z]{2}' /tmp/provider-smoke.log; then\n echo \"::error::Provider smoke test produced no output\"\n cat /tmp/provider-smoke.log\n exit 1\n fi\n\n echo \"Provider smoke test passed\"\n\n - name: Write job summary\n if: always()\n run: |\n echo \"## Provider Smoke Gate\" >> $GITHUB_STEP_SUMMARY\n echo \"\" >> $GITHUB_STEP_SUMMARY\n if [ -f /tmp/provider-smoke.log ]; then\n echo '```' >> $GITHUB_STEP_SUMMARY\n head -20 /tmp/provider-smoke.log >> $GITHUB_STEP_SUMMARY\n echo '```' >> $GITHUB_STEP_SUMMARY\n fi\n\n create-release:\n name: Create GitHub Release\n needs: [resolve-version, test-gate, provider-smoke-gate]\n runs-on: ubuntu-latest\n permissions:\n contents: write\n steps:\n - uses: actions/checkout@v6\n with:\n fetch-depth: 0\n token: ${{ secrets.ALIUS_RELEASE_TOKEN }}\n\n - name: Create tag (after test-gate passed)\n run: |\n TAG=\"${{ needs.resolve-version.outputs.tag_name }}\"\n if git ls-remote origin \"refs/tags/${TAG}\" | grep -q .; then\n echo \"Tag ${TAG} already exists\"\n else\n git tag \"$TAG\"\n git push origin \"$TAG\"\n echo \"Created tag ${TAG}\"\n fi\n\n - name: Create or verify release\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n TAG=\"${{ needs.resolve-version.outputs.tag_name }}\"\n VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n\n if gh release view \"$TAG\" >/dev/null 2>&1; then\n echo \"Release $TAG already exists, skipping creation\"\n else\n TAG_SHA=$(git ls-remote origin \"refs/tags/${TAG}\" | cut -f1)\n if [ -z \"$TAG_SHA\" ]; then\n TAG_SHA=\"$GITHUB_SHA\"\n fi\n echo \"Creating release $TAG at commit $TAG_SHA...\"\n gh release create \"$TAG\" \\\n --title \"v${VERSION}\" \\\n --generate-notes \\\n --target \"$TAG_SHA\"\n fi\n\n build-linux:\n name: Build Linux\n needs: [resolve-version, create-release]\n runs-on: ubuntu-latest\n permissions:\n contents: write\n steps:\n - uses: actions/checkout@v6\n\n - name: Install Rust\n uses: dtolnay/rust-toolchain@stable\n\n - name: Verify and fix version consistency\n run: |\n TAG_VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n CARGO_VERSION=$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[] | select(.name==\"alius-cli\") | .version')\n echo \"Tag version: $TAG_VERSION\"\n echo \"Cargo version: $CARGO_VERSION\"\n if [ \"$TAG_VERSION\" != \"$CARGO_VERSION\" ]; then\n echo \"::warning::Tag version $TAG_VERSION != Cargo version $CARGO_VERSION, auto-fixing Cargo.toml\"\n sed -i \"s/^version = \\\".*\\\"/version = \\\"${TAG_VERSION}\\\"/\" Cargo.toml\n echo \"Cargo.toml updated to version $TAG_VERSION\"\n fi\n\n - name: Cache Rust artifacts\n uses: Swatinem/rust-cache@v2\n with:\n key: ${{ needs.resolve-version.outputs.version }}-x86_64-linux\n workspaces: \". -> target\"\n cache-on-failure: true\n\n - name: Build release binary\n run: cargo build -p alius-cli --bin alius --release --locked\n\n - name: Scan release binary for test symbols\n run: |\n SCAN=$(strings target/release/alius | grep -cE 'FakeProvider|FakeTool|TuiTestHarness|CoreRuntimeHarness|VecEventSource|testing::|testkit' || true)\n if [ \"$SCAN\" -gt 0 ]; then\n echo \"ERROR: Test symbols found in release binary\"\n exit 1\n fi\n\n - name: Prepare artifacts\n run: |\n cd target/release\n tar -czvf ../../alius-linux-x64.tar.gz alius\n cd ../..\n\n - name: Upload artifact to release\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n gh release upload ${{ needs.resolve-version.outputs.tag_name }} \\\n alius-linux-x64.tar.gz --clobber\n\n build-macos-x64:\n name: Build macOS x64\n needs: [resolve-version, create-release]\n runs-on: macos-26-intel\n steps:\n - uses: actions/checkout@v6\n\n - name: Verify and fix version consistency\n run: |\n TAG_VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n CARGO_VERSION=$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[] | select(.name==\"alius-cli\") | .version')\n echo \"Tag version: $TAG_VERSION\"\n echo \"Cargo version: $CARGO_VERSION\"\n if [ \"$TAG_VERSION\" != \"$CARGO_VERSION\" ]; then\n echo \"::warning::Tag version $TAG_VERSION != Cargo version $CARGO_VERSION, auto-fixing Cargo.toml\"\n sed -i '' \"s/^version = \\\".*\\\"/version = \\\"${TAG_VERSION}\\\"/\" Cargo.toml\n echo \"Cargo.toml updated to version $TAG_VERSION\"\n fi\n\n - name: Install Rust\n uses: dtolnay/rust-toolchain@stable\n\n - name: Setup sccache\n uses: mozilla-actions/sccache-action@v0.0.9\n\n - name: Cache Rust artifacts\n uses: Swatinem/rust-cache@v2\n with:\n key: ${{ needs.resolve-version.outputs.version }}-x86_64-darwin\n cache-on-failure: true\n\n - name: Build release binary\n run: cargo build -p alius-cli --bin alius --release --locked\n\n - name: Scan release binary for test symbols\n run: |\n SCAN=$(strings target/release/alius | grep -cE 'FakeProvider|FakeTool|TuiTestHarness|CoreRuntimeHarness|VecEventSource|testing::|testkit' || true)\n if [ \"$SCAN\" -gt 0 ]; then\n echo \"ERROR: Test symbols found in release binary\"\n exit 1\n fi\n\n - name: Prepare artifact\n run: |\n cd target/release\n tar -czvf ../../alius-macos-x64.tar.gz alius\n cd ../..\n\n - name: Upload artifact to release\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n gh release upload ${{ needs.resolve-version.outputs.tag_name }} \\\n alius-macos-x64.tar.gz --clobber\n\n build-macos-arm64:\n name: Build macOS ARM64\n needs: [resolve-version, create-release]\n runs-on: macos-26\n steps:\n - uses: actions/checkout@v6\n\n - name: Verify and fix version consistency\n run: |\n TAG_VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n CARGO_VERSION=$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[] | select(.name==\"alius-cli\") | .version')\n echo \"Tag version: $TAG_VERSION\"\n echo \"Cargo version: $CARGO_VERSION\"\n if [ \"$TAG_VERSION\" != \"$CARGO_VERSION\" ]; then\n echo \"::warning::Tag version $TAG_VERSION != Cargo version $CARGO_VERSION, auto-fixing Cargo.toml\"\n sed -i '' \"s/^version = \\\".*\\\"/version = \\\"${TAG_VERSION}\\\"/\" Cargo.toml\n echo \"Cargo.toml updated to version $TAG_VERSION\"\n fi\n\n - name: Install Rust\n uses: dtolnay/rust-toolchain@stable\n\n - name: Setup sccache\n uses: mozilla-actions/sccache-action@v0.0.9\n\n - name: Cache Rust artifacts\n uses: Swatinem/rust-cache@v2\n with:\n key: ${{ needs.resolve-version.outputs.version }}-aarch64-darwin\n cache-on-failure: true\n\n - name: Build release binary\n run: cargo build -p alius-cli --bin alius --release --locked\n\n - name: Scan release binary for test symbols\n run: |\n SCAN=$(strings target/release/alius | grep -cE 'FakeProvider|FakeTool|TuiTestHarness|CoreRuntimeHarness|VecEventSource|testing::|testkit' || true)\n if [ \"$SCAN\" -gt 0 ]; then\n echo \"ERROR: Test symbols found in release binary\"\n exit 1\n fi\n\n - name: Prepare artifact\n run: |\n cd target/release\n tar -czvf ../../alius-macos-arm64.tar.gz alius\n cd ../..\n\n - name: Upload artifact to release\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n gh release upload ${{ needs.resolve-version.outputs.tag_name }} \\\n alius-macos-arm64.tar.gz --clobber\n\n build-windows:\n name: Build Windows\n needs: [resolve-version, create-release]\n runs-on: windows-2025-vs2026\n steps:\n - uses: actions/checkout@v6\n\n - name: Verify and fix version consistency\n shell: bash\n run: |\n TAG_VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n CARGO_VERSION=$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[] | select(.name==\"alius-cli\") | .version')\n echo \"Tag version: $TAG_VERSION\"\n echo \"Cargo version: $CARGO_VERSION\"\n if [ \"$TAG_VERSION\" != \"$CARGO_VERSION\" ]; then\n echo \"::warning::Tag version $TAG_VERSION != Cargo version $CARGO_VERSION, auto-fixing Cargo.toml\"\n sed -i \"s/^version = \\\".*\\\"/version = \\\"${TAG_VERSION}\\\"/\" Cargo.toml\n echo \"Cargo.toml updated to version $TAG_VERSION\"\n fi\n\n - name: Install Rust\n uses: dtolnay/rust-toolchain@stable\n\n - name: Setup sccache\n uses: mozilla-actions/sccache-action@v0.0.9\n\n - name: Cache Rust artifacts\n uses: Swatinem/rust-cache@v2\n with:\n key: ${{ needs.resolve-version.outputs.version }}-x86_64-windows\n cache-on-failure: true\n\n - name: Build release binary\n run: cargo build -p alius-cli --bin alius --release --locked\n\n - name: Scan release binary for test symbols\n shell: bash\n run: |\n SCAN=$(strings target/release/alius.exe | grep -cE 'FakeProvider|FakeTool|TuiTestHarness|CoreRuntimeHarness|VecEventSource|testing::|testkit' || true)\n if [ \"$SCAN\" -gt 0 ]; then\n echo \"ERROR: Test symbols found in release binary\"\n exit 1\n fi\n\n - name: Prepare artifact\n run: |\n cd target/release\n 7z a ../../alius-windows-x64.zip alius.exe\n cd ../..\n\n - name: Upload artifact to release\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n gh release upload ${{ needs.resolve-version.outputs.tag_name }} \\\n alius-windows-x64.zip --clobber\n\n verify-assets:\n name: Verify Release Assets\n needs:\n - resolve-version\n - build-linux\n - build-macos-x64\n - build-macos-arm64\n - build-windows\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v6\n\n - name: Verify all assets exist\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n TAG: ${{ needs.resolve-version.outputs.tag_name }}\n run: |\n echo \"Verifying assets for release $TAG...\"\n\n ASSETS=$(gh release view \"$TAG\" --json assets --jq '.assets[].name')\n echo \"Found assets:\"\n echo \"$ASSETS\"\n\n REQUIRED=(\n \"alius-linux-x64.tar.gz\"\n \"alius-macos-x64.tar.gz\"\n \"alius-macos-arm64.tar.gz\"\n \"alius-windows-x64.zip\"\n )\n\n MISSING=0\n for asset in \"${REQUIRED[@]}\"; do\n if ! echo \"$ASSETS\" | grep -q \"$asset\"; then\n echo \"::error::Missing required asset: $asset\"\n MISSING=1\n fi\n done\n\n if [ $MISSING -ne 0 ]; then\n echo \"::error::Some required assets are missing\"\n exit 1\n fi\n\n echo \"All required assets verified\"\n\n publish-npm:\n name: Publish npm\n needs:\n - resolve-version\n - verify-assets\n runs-on: ubuntu-latest\n permissions:\n contents: read\n id-token: write\n steps:\n - uses: actions/checkout@v6\n\n - name: Setup Node.js\n uses: actions/setup-node@v6\n with:\n node-version: '24'\n registry-url: 'https://registry.npmjs.org'\n package-manager-cache: false\n\n - name: Verify Trusted Publishing prerequisites\n run: |\n node --version\n npm --version\n node - <<'NODE'\n const { execSync } = require('child_process');\n\n function assertAtLeast(name, actual, minimum) {\n const got = actual.replace(/^v/, '').split('.').map(Number);\n const min = minimum.split('.').map(Number);\n for (let i = 0; i < min.length; i += 1) {\n if ((got[i] || 0) > min[i]) return;\n if ((got[i] || 0) < min[i]) {\n throw new Error(`${name} ${actual} is below required ${minimum}`);\n }\n }\n }\n\n assertAtLeast('Node.js', process.version, '22.14.0');\n assertAtLeast('npm', execSync('npm --version', { encoding: 'utf8' }).trim(), '11.5.1');\n NODE\n\n - name: Prepare npm packages\n run: |\n VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n RELEASE_TAG=\"${{ needs.resolve-version.outputs.tag_name }}\"\n node npm-packages/generate-packages.js \"$VERSION\" \"$RELEASE_TAG\"\n node npm-packages/verify-release-version.js \"$VERSION\" \"$RELEASE_TAG\"\n\n - name: Publish platform packages\n run: |\n VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n\n for pkg in alius-darwin-x64 alius-darwin-arm64 alius-linux-x64 alius-win32-x64; do\n if npm view \"@alius-tech/$pkg@$VERSION\" version >/dev/null 2>&1; then\n echo \"Skipping @alius-tech/$pkg@$VERSION (already published)\"\n continue\n fi\n echo \"Publishing @alius-tech/$pkg@$VERSION...\"\n cd \"npm-packages/$pkg\"\n npm publish --access public --provenance --tag latest\n cd ../..\n done\n\n - name: Publish main package\n run: |\n VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n\n if npm view \"@alius-tech/alius@$VERSION\" version >/dev/null 2>&1; then\n echo \"Skipping @alius-tech/alius@$VERSION (already published)\"\n exit 0\n fi\n\n cd npm-packages/alius\n npm publish --access public --provenance --tag latest\n\n update-homebrew:\n name: Update Homebrew\n needs:\n - resolve-version\n - verify-assets\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v6\n\n - name: Download release artifacts\n run: |\n RELEASE_TAG=\"${{ needs.resolve-version.outputs.tag_name }}\"\n BASE_URL=\"https://github.com/AliusTech/alius/releases/download/${RELEASE_TAG}\"\n mkdir -p artifacts\n\n curl -fsSL -o artifacts/alius-macos-arm64.tar.gz \"${BASE_URL}/alius-macos-arm64.tar.gz\"\n curl -fsSL -o artifacts/alius-macos-x64.tar.gz \"${BASE_URL}/alius-macos-x64.tar.gz\"\n curl -fsSL -o artifacts/alius-linux-x64.tar.gz \"${BASE_URL}/alius-linux-x64.tar.gz\"\n\n - name: Compute SHA256 checksums\n id: sha256\n run: |\n echo \"arm64=$(sha256sum artifacts/alius-macos-arm64.tar.gz | cut -d' ' -f1)\" >> $GITHUB_OUTPUT\n echo \"x64=$(sha256sum artifacts/alius-macos-x64.tar.gz | cut -d' ' -f1)\" >> $GITHUB_OUTPUT\n echo \"linux_x64=$(sha256sum artifacts/alius-linux-x64.tar.gz | cut -d' ' -f1)\" >> $GITHUB_OUTPUT\n\n - name: Checkout homebrew tap\n uses: actions/checkout@v6\n with:\n repository: AliusTech/homebrew-tap\n token: ${{ secrets.TAP_GITHUB_TOKEN }}\n path: tap\n\n - name: Check if version already published\n id: check\n run: |\n VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n FORMULA=\"tap/Formula/alius.rb\"\n if [ -f \"$FORMULA\" ] && grep -q \"version \\\"${VERSION}\\\"\" \"$FORMULA\"; then\n echo \"Homebrew formula already at version ${VERSION}, skipping\"\n echo \"skip=true\" >> $GITHUB_OUTPUT\n else\n echo \"skip=false\" >> $GITHUB_OUTPUT\n fi\n\n - name: Update formula\n if: steps.check.outputs.skip != 'true'\n env:\n VERSION: ${{ needs.resolve-version.outputs.version }}\n SHA256_ARM64: ${{ steps.sha256.outputs.arm64 }}\n SHA256_X64: ${{ steps.sha256.outputs.x64 }}\n SHA256_LINUX_X64: ${{ steps.sha256.outputs.linux_x64 }}\n run: |\n rm -rf tap/Formula\n mkdir -p tap/Formula\n cat > tap/Formula/alius.rb <<'TEMPLATE'\n class Alius < Formula\n desc \"LLM Agent CLI - Software Self-Evolution\"\n homepage \"https://github.com/AliusTech/alius\"\n version \"%%VERSION%%\"\n license \"MIT\"\n\n on_macos do\n if Hardware::CPU.arm?\n url \"https://github.com/AliusTech/alius/releases/download/%%RELEASE_TAG%%/alius-macos-arm64.tar.gz\"\n sha256 \"%%SHA256_ARM64%%\"\n else\n url \"https://github.com/AliusTech/alius/releases/download/%%RELEASE_TAG%%/alius-macos-x64.tar.gz\"\n sha256 \"%%SHA256_X64%%\"\n end\n end\n\n on_linux do\n url \"https://github.com/AliusTech/alius/releases/download/%%RELEASE_TAG%%/alius-linux-x64.tar.gz\"\n sha256 \"%%SHA256_LINUX_X64%%\"\n end\n\n def install\n bin.install \"alius\"\n end\n\n test do\n system bin/\"alius\", \"--version\"\n end\n end\n TEMPLATE\n\n sed -i \"s/%%VERSION%%/${VERSION}/g\" tap/Formula/alius.rb\n sed -i \"s/%%RELEASE_TAG%%/${{ needs.resolve-version.outputs.tag_name }}/g\" tap/Formula/alius.rb\n sed -i \"s/%%SHA256_ARM64%%/${SHA256_ARM64}/g\" tap/Formula/alius.rb\n sed -i \"s/%%SHA256_X64%%/${SHA256_X64}/g\" tap/Formula/alius.rb\n sed -i \"s/%%SHA256_LINUX_X64%%/${SHA256_LINUX_X64}/g\" tap/Formula/alius.rb\n\n - name: Push formula update\n if: steps.check.outputs.skip != 'true'\n run: |\n VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n cd tap\n git config user.name \"github-actions[bot]\"\n git config user.email \"github-actions[bot]@users.noreply.github.com\"\n git add Formula/alius.rb\n git diff --staged --quiet && exit 0\n git commit -m \"Update alius to ${VERSION}\"\n git push\n"}, "f163efc4f02d288a7d629e65d103afb83349cc90": {"t": 1785750731.8545208, "n": 22860, "h": "6153c8d5e69a4dc5", "c": "name: Release\n\non:\n push:\n branches:\n - release\n\npermissions:\n contents: write\n id-token: write\n\nconcurrency:\n group: release-${{ github.ref }}\n cancel-in-progress: false\n\nenv:\n CARGO_TERM_COLOR: always\n CARGO_INCREMENTAL: \"0\"\n\njobs:\n resolve-version:\n name: Resolve Version\n runs-on: ubuntu-latest\n outputs:\n version: ${{ steps.version.outputs.version }}\n tag_name: ${{ steps.version.outputs.tag_name }}\n steps:\n - uses: actions/checkout@v6\n with:\n fetch-depth: 0\n token: ${{ secrets.ALIUS_RELEASE_TOKEN }}\n\n - name: Read version from VERSION.txt\n id: version\n run: |\n VERSION=$(cat VERSION.txt | tr -d '[:space:]')\n echo \"version=${VERSION}\" >> $GITHUB_OUTPUT\n echo \"tag_name=v${VERSION}\" >> $GITHUB_OUTPUT\n\n - name: Check version consistency\n run: bash scripts/check-version.sh\n\n test-gate:\n name: Release Test Gate\n needs: resolve-version\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v6\n\n - name: Install Rust\n uses: dtolnay/rust-toolchain@stable\n with:\n components: rustfmt, clippy, llvm-tools-preview\n\n - name: Install cargo-llvm-cov\n run: cargo install cargo-llvm-cov --locked\n\n - name: Cache Rust artifacts\n uses: Swatinem/rust-cache@v2\n with:\n key: release-test-gate\n\n - name: Check formatting\n run: cargo fmt --all -- --check\n\n - name: Run clippy\n run: cargo clippy --locked --workspace --all-targets --features testing -- -D warnings\n\n - name: Run tests with coverage\n run: |\n mkdir -p target/ci-reports target/llvm-cov\n cargo llvm-cov --locked --workspace --features testing \\\n --no-report -- --test-threads=1 --nocapture 2>&1 | tee target/ci-reports/test.log\n # Parse test result lines\n grep 'test result:' target/ci-reports/test.log > target/ci-reports/test-result-lines.txt || true\n # Write summary env\n PASSED=$(grep -oP '(\\d+) passed' target/ci-reports/test-result-lines.txt | tail -1 | grep -oP '\\d+' || echo 0)\n FAILED=$(grep -oP '(\\d+) failed' target/ci-reports/test-result-lines.txt | tail -1 | grep -oP '\\d+' || echo 0)\n echo \"tests_passed=${PASSED}\" > target/ci-reports/test-summary.env\n echo \"tests_failed=${FAILED}\" >> target/ci-reports/test-summary.env\n\n - name: Generate coverage reports\n run: |\n COV_IGNORE='--ignore-filename-regex (/tests/|/testing\\.rs$|/testkit/|state_machine_tests\\.rs$)'\n cargo llvm-cov report --lcov $COV_IGNORE --output-path target/llvm-cov/lcov.info\n cargo llvm-cov report --html $COV_IGNORE --output-dir target/llvm-cov/html\n # Extract TOTAL line for compact job summary\n cargo llvm-cov report $COV_IGNORE | grep \"^TOTAL\" | tee target/llvm-cov/summary.txt\n\n - name: Enforce coverage threshold\n run: |\n # Staged gate: current baseline ~67.5%. See docs/standards/validation.md §Staged Coverage Threshold.\n cargo llvm-cov report --ignore-filename-regex '(/tests/|/testing\\.rs$|/testkit/|state_machine_tests\\.rs$)' --fail-under-lines 65\n\n - name: Build release binary\n run: cargo build -p alius-cli --bin alius --release --locked\n\n - name: Scan release binary for test symbols\n run: |\n SCAN=$(strings target/release/alius | grep -cE 'FakeProvider|FakeTool|TuiTestHarness|CoreRuntimeHarness|VecEventSource|testing::|testkit' || true)\n if [ \"$SCAN\" -gt 0 ]; then\n echo \"ERROR: Test symbols found in release binary\"\n strings target/release/alius | grep -E 'FakeProvider|FakeTool|TuiTestHarness|CoreRuntimeHarness|VecEventSource|testing::|testkit'\n exit 1\n fi\n echo \"Release binary clean — no test symbols found\"\n\n - name: Install cargo-audit\n uses: taiki-e/install-action@v2\n with:\n tool: cargo-audit\n\n - name: Run security audit\n # Exceptions documented in docs/audit-exceptions.md\n run: cargo audit --deny warnings --ignore RUSTSEC-2025-0012 --ignore RUSTSEC-2024-0384 --ignore RUSTSEC-2024-0436 --ignore RUSTSEC-2026-0002\n\n - name: Write job summary\n if: always()\n run: |\n echo \"## Release Test Gate\" >> $GITHUB_STEP_SUMMARY\n echo \"\" >> $GITHUB_STEP_SUMMARY\n if [ -f target/ci-reports/test-result-lines.txt ]; then\n echo '```' >> $GITHUB_STEP_SUMMARY\n cat target/ci-reports/test-result-lines.txt >> $GITHUB_STEP_SUMMARY\n echo '```' >> $GITHUB_STEP_SUMMARY\n fi\n if [ -f target/llvm-cov/summary.txt ]; then\n echo \"\" >> $GITHUB_STEP_SUMMARY\n echo \"### Coverage\" >> $GITHUB_STEP_SUMMARY\n echo '```' >> $GITHUB_STEP_SUMMARY\n cat target/llvm-cov/summary.txt >> $GITHUB_STEP_SUMMARY\n echo '```' >> $GITHUB_STEP_SUMMARY\n fi\n\n - name: Upload test artifacts\n if: always()\n uses: actions/upload-artifact@v4\n with:\n name: release-test-gate-artifacts\n path: |\n target/ci-reports/\n target/llvm-cov/\n retention-days: 30\n\n create-release:\n name: Create GitHub Release\n needs: [resolve-version, test-gate]\n runs-on: ubuntu-latest\n permissions:\n contents: write\n steps:\n - uses: actions/checkout@v6\n with:\n fetch-depth: 0\n token: ${{ secrets.ALIUS_RELEASE_TOKEN }}\n\n - name: Create tag (after test-gate passed)\n run: |\n TAG=\"${{ needs.resolve-version.outputs.tag_name }}\"\n if git ls-remote origin \"refs/tags/${TAG}\" | grep -q .; then\n echo \"Tag ${TAG} already exists\"\n else\n git tag \"$TAG\"\n git push origin \"$TAG\"\n echo \"Created tag ${TAG}\"\n fi\n\n - name: Create or verify release\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n TAG=\"${{ needs.resolve-version.outputs.tag_name }}\"\n VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n\n if gh release view \"$TAG\" >/dev/null 2>&1; then\n echo \"Release $TAG already exists, skipping creation\"\n else\n TAG_SHA=$(git ls-remote origin \"refs/tags/${TAG}\" | cut -f1)\n if [ -z \"$TAG_SHA\" ]; then\n TAG_SHA=\"$GITHUB_SHA\"\n fi\n echo \"Creating release $TAG at commit $TAG_SHA...\"\n gh release create \"$TAG\" \\\n --title \"v${VERSION}\" \\\n --generate-notes \\\n --target \"$TAG_SHA\"\n fi\n\n build-linux:\n name: Build Linux\n needs: [resolve-version, create-release]\n runs-on: ubuntu-latest\n permissions:\n contents: write\n steps:\n - uses: actions/checkout@v6\n\n - name: Install Rust\n uses: dtolnay/rust-toolchain@stable\n\n - name: Verify and fix version consistency\n run: |\n TAG_VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n CARGO_VERSION=$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[] | select(.name==\"alius-cli\") | .version')\n echo \"Tag version: $TAG_VERSION\"\n echo \"Cargo version: $CARGO_VERSION\"\n if [ \"$TAG_VERSION\" != \"$CARGO_VERSION\" ]; then\n echo \"::warning::Tag version $TAG_VERSION != Cargo version $CARGO_VERSION, auto-fixing Cargo.toml\"\n sed -i \"s/^version = \\\".*\\\"/version = \\\"${TAG_VERSION}\\\"/\" Cargo.toml\n echo \"Cargo.toml updated to version $TAG_VERSION\"\n fi\n\n - name: Cache Rust artifacts\n uses: Swatinem/rust-cache@v2\n with:\n key: ${{ needs.resolve-version.outputs.version }}-x86_64-linux\n workspaces: \". -> target\"\n cache-on-failure: true\n\n - name: Build release binary\n run: cargo build -p alius-cli --bin alius --release --locked\n\n - name: Scan release binary for test symbols\n run: |\n SCAN=$(strings target/release/alius | grep -cE 'FakeProvider|FakeTool|TuiTestHarness|CoreRuntimeHarness|VecEventSource|testing::|testkit' || true)\n if [ \"$SCAN\" -gt 0 ]; then\n echo \"ERROR: Test symbols found in release binary\"\n exit 1\n fi\n\n - name: Prepare artifacts\n run: |\n cd target/release\n tar -czvf ../../alius-linux-x64.tar.gz alius\n cd ../..\n\n - name: Upload artifact to release\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n gh release upload ${{ needs.resolve-version.outputs.tag_name }} \\\n alius-linux-x64.tar.gz --clobber\n\n build-macos-x64:\n name: Build macOS x64\n needs: [resolve-version, create-release]\n runs-on: macos-26-intel\n steps:\n - uses: actions/checkout@v6\n\n - name: Verify and fix version consistency\n run: |\n TAG_VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n CARGO_VERSION=$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[] | select(.name==\"alius-cli\") | .version')\n echo \"Tag version: $TAG_VERSION\"\n echo \"Cargo version: $CARGO_VERSION\"\n if [ \"$TAG_VERSION\" != \"$CARGO_VERSION\" ]; then\n echo \"::warning::Tag version $TAG_VERSION != Cargo version $CARGO_VERSION, auto-fixing Cargo.toml\"\n sed -i '' \"s/^version = \\\".*\\\"/version = \\\"${TAG_VERSION}\\\"/\" Cargo.toml\n echo \"Cargo.toml updated to version $TAG_VERSION\"\n fi\n\n - name: Install Rust\n uses: dtolnay/rust-toolchain@stable\n\n - name: Setup sccache\n uses: mozilla-actions/sccache-action@v0.0.9\n\n - name: Cache Rust artifacts\n uses: Swatinem/rust-cache@v2\n with:\n key: ${{ needs.resolve-version.outputs.version }}-x86_64-darwin\n cache-on-failure: true\n\n - name: Build release binary\n run: cargo build -p alius-cli --bin alius --release --locked\n\n - name: Scan release binary for test symbols\n run: |\n SCAN=$(strings target/release/alius | grep -cE 'FakeProvider|FakeTool|TuiTestHarness|CoreRuntimeHarness|VecEventSource|testing::|testkit' || true)\n if [ \"$SCAN\" -gt 0 ]; then\n echo \"ERROR: Test symbols found in release binary\"\n exit 1\n fi\n\n - name: Prepare artifact\n run: |\n cd target/release\n tar -czvf ../../alius-macos-x64.tar.gz alius\n cd ../..\n\n - name: Upload artifact to release\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n gh release upload ${{ needs.resolve-version.outputs.tag_name }} \\\n alius-macos-x64.tar.gz --clobber\n\n build-macos-arm64:\n name: Build macOS ARM64\n needs: [resolve-version, create-release]\n runs-on: macos-26\n steps:\n - uses: actions/checkout@v6\n\n - name: Verify and fix version consistency\n run: |\n TAG_VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n CARGO_VERSION=$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[] | select(.name==\"alius-cli\") | .version')\n echo \"Tag version: $TAG_VERSION\"\n echo \"Cargo version: $CARGO_VERSION\"\n if [ \"$TAG_VERSION\" != \"$CARGO_VERSION\" ]; then\n echo \"::warning::Tag version $TAG_VERSION != Cargo version $CARGO_VERSION, auto-fixing Cargo.toml\"\n sed -i '' \"s/^version = \\\".*\\\"/version = \\\"${TAG_VERSION}\\\"/\" Cargo.toml\n echo \"Cargo.toml updated to version $TAG_VERSION\"\n fi\n\n - name: Install Rust\n uses: dtolnay/rust-toolchain@stable\n\n - name: Setup sccache\n uses: mozilla-actions/sccache-action@v0.0.9\n\n - name: Cache Rust artifacts\n uses: Swatinem/rust-cache@v2\n with:\n key: ${{ needs.resolve-version.outputs.version }}-aarch64-darwin\n cache-on-failure: true\n\n - name: Build release binary\n run: cargo build -p alius-cli --bin alius --release --locked\n\n - name: Scan release binary for test symbols\n run: |\n SCAN=$(strings target/release/alius | grep -cE 'FakeProvider|FakeTool|TuiTestHarness|CoreRuntimeHarness|VecEventSource|testing::|testkit' || true)\n if [ \"$SCAN\" -gt 0 ]; then\n echo \"ERROR: Test symbols found in release binary\"\n exit 1\n fi\n\n - name: Prepare artifact\n run: |\n cd target/release\n tar -czvf ../../alius-macos-arm64.tar.gz alius\n cd ../..\n\n - name: Upload artifact to release\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n gh release upload ${{ needs.resolve-version.outputs.tag_name }} \\\n alius-macos-arm64.tar.gz --clobber\n\n build-windows:\n name: Build Windows\n needs: [resolve-version, create-release]\n runs-on: windows-2025-vs2026\n steps:\n - uses: actions/checkout@v6\n\n - name: Verify and fix version consistency\n shell: bash\n run: |\n TAG_VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n CARGO_VERSION=$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[] | select(.name==\"alius-cli\") | .version')\n echo \"Tag version: $TAG_VERSION\"\n echo \"Cargo version: $CARGO_VERSION\"\n if [ \"$TAG_VERSION\" != \"$CARGO_VERSION\" ]; then\n echo \"::warning::Tag version $TAG_VERSION != Cargo version $CARGO_VERSION, auto-fixing Cargo.toml\"\n sed -i \"s/^version = \\\".*\\\"/version = \\\"${TAG_VERSION}\\\"/\" Cargo.toml\n echo \"Cargo.toml updated to version $TAG_VERSION\"\n fi\n\n - name: Install Rust\n uses: dtolnay/rust-toolchain@stable\n\n - name: Setup sccache\n uses: mozilla-actions/sccache-action@v0.0.9\n\n - name: Cache Rust artifacts\n uses: Swatinem/rust-cache@v2\n with:\n key: ${{ needs.resolve-version.outputs.version }}-x86_64-windows\n cache-on-failure: true\n\n - name: Build release binary\n run: cargo build -p alius-cli --bin alius --release --locked\n\n - name: Scan release binary for test symbols\n shell: bash\n run: |\n SCAN=$(strings target/release/alius.exe | grep -cE 'FakeProvider|FakeTool|TuiTestHarness|CoreRuntimeHarness|VecEventSource|testing::|testkit' || true)\n if [ \"$SCAN\" -gt 0 ]; then\n echo \"ERROR: Test symbols found in release binary\"\n exit 1\n fi\n\n - name: Prepare artifact\n run: |\n cd target/release\n 7z a ../../alius-windows-x64.zip alius.exe\n cd ../..\n\n - name: Upload artifact to release\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n gh release upload ${{ needs.resolve-version.outputs.tag_name }} \\\n alius-windows-x64.zip --clobber\n\n verify-assets:\n name: Verify Release Assets\n needs:\n - resolve-version\n - build-linux\n - build-macos-x64\n - build-macos-arm64\n - build-windows\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v6\n\n - name: Verify all assets exist\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n TAG: ${{ needs.resolve-version.outputs.tag_name }}\n run: |\n echo \"Verifying assets for release $TAG...\"\n\n ASSETS=$(gh release view \"$TAG\" --json assets --jq '.assets[].name')\n echo \"Found assets:\"\n echo \"$ASSETS\"\n\n REQUIRED=(\n \"alius-linux-x64.tar.gz\"\n \"alius-macos-x64.tar.gz\"\n \"alius-macos-arm64.tar.gz\"\n \"alius-windows-x64.zip\"\n )\n\n MISSING=0\n for asset in \"${REQUIRED[@]}\"; do\n if ! echo \"$ASSETS\" | grep -q \"$asset\"; then\n echo \"::error::Missing required asset: $asset\"\n MISSING=1\n fi\n done\n\n if [ $MISSING -ne 0 ]; then\n echo \"::error::Some required assets are missing\"\n exit 1\n fi\n\n echo \"All required assets verified\"\n\n publish-npm:\n name: Publish npm\n needs:\n - resolve-version\n - verify-assets\n runs-on: ubuntu-latest\n permissions:\n contents: read\n id-token: write\n steps:\n - uses: actions/checkout@v6\n\n - name: Setup Node.js\n uses: actions/setup-node@v6\n with:\n node-version: '24'\n registry-url: 'https://registry.npmjs.org'\n package-manager-cache: false\n\n - name: Verify Trusted Publishing prerequisites\n run: |\n node --version\n npm --version\n node - <<'NODE'\n const { execSync } = require('child_process');\n\n function assertAtLeast(name, actual, minimum) {\n const got = actual.replace(/^v/, '').split('.').map(Number);\n const min = minimum.split('.').map(Number);\n for (let i = 0; i < min.length; i += 1) {\n if ((got[i] || 0) > min[i]) return;\n if ((got[i] || 0) < min[i]) {\n throw new Error(`${name} ${actual} is below required ${minimum}`);\n }\n }\n }\n\n assertAtLeast('Node.js', process.version, '22.14.0');\n assertAtLeast('npm', execSync('npm --version', { encoding: 'utf8' }).trim(), '11.5.1');\n NODE\n\n - name: Prepare npm packages\n run: |\n VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n RELEASE_TAG=\"${{ needs.resolve-version.outputs.tag_name }}\"\n node npm-packages/generate-packages.js \"$VERSION\" \"$RELEASE_TAG\"\n node npm-packages/verify-release-version.js \"$VERSION\" \"$RELEASE_TAG\"\n\n - name: Publish platform packages\n run: |\n VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n\n for pkg in alius-darwin-x64 alius-darwin-arm64 alius-linux-x64 alius-win32-x64; do\n if npm view \"@alius-tech/$pkg@$VERSION\" version >/dev/null 2>&1; then\n echo \"Skipping @alius-tech/$pkg@$VERSION (already published)\"\n continue\n fi\n echo \"Publishing @alius-tech/$pkg@$VERSION...\"\n cd \"npm-packages/$pkg\"\n npm publish --access public --provenance --tag latest\n cd ../..\n done\n\n - name: Publish main package\n run: |\n VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n\n if npm view \"@alius-tech/alius@$VERSION\" version >/dev/null 2>&1; then\n echo \"Skipping @alius-tech/alius@$VERSION (already published)\"\n exit 0\n fi\n\n cd npm-packages/alius\n npm publish --access public --provenance --tag latest\n\n update-homebrew:\n name: Update Homebrew\n needs:\n - resolve-version\n - verify-assets\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v6\n\n - name: Download release artifacts\n run: |\n RELEASE_TAG=\"${{ needs.resolve-version.outputs.tag_name }}\"\n BASE_URL=\"https://github.com/AliusTech/alius/releases/download/${RELEASE_TAG}\"\n mkdir -p artifacts\n\n curl -fsSL -o artifacts/alius-macos-arm64.tar.gz \"${BASE_URL}/alius-macos-arm64.tar.gz\"\n curl -fsSL -o artifacts/alius-macos-x64.tar.gz \"${BASE_URL}/alius-macos-x64.tar.gz\"\n curl -fsSL -o artifacts/alius-linux-x64.tar.gz \"${BASE_URL}/alius-linux-x64.tar.gz\"\n\n - name: Compute SHA256 checksums\n id: sha256\n run: |\n echo \"arm64=$(sha256sum artifacts/alius-macos-arm64.tar.gz | cut -d' ' -f1)\" >> $GITHUB_OUTPUT\n echo \"x64=$(sha256sum artifacts/alius-macos-x64.tar.gz | cut -d' ' -f1)\" >> $GITHUB_OUTPUT\n echo \"linux_x64=$(sha256sum artifacts/alius-linux-x64.tar.gz | cut -d' ' -f1)\" >> $GITHUB_OUTPUT\n\n - name: Checkout homebrew tap\n uses: actions/checkout@v6\n with:\n repository: AliusTech/homebrew-tap\n token: ${{ secrets.TAP_GITHUB_TOKEN }}\n path: tap\n\n - name: Check if version already published\n id: check\n run: |\n VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n FORMULA=\"tap/Formula/alius.rb\"\n if [ -f \"$FORMULA\" ] && grep -q \"version \\\"${VERSION}\\\"\" \"$FORMULA\"; then\n echo \"Homebrew formula already at version ${VERSION}, skipping\"\n echo \"skip=true\" >> $GITHUB_OUTPUT\n else\n echo \"skip=false\" >> $GITHUB_OUTPUT\n fi\n\n - name: Update formula\n if: steps.check.outputs.skip != 'true'\n env:\n VERSION: ${{ needs.resolve-version.outputs.version }}\n SHA256_ARM64: ${{ steps.sha256.outputs.arm64 }}\n SHA256_X64: ${{ steps.sha256.outputs.x64 }}\n SHA256_LINUX_X64: ${{ steps.sha256.outputs.linux_x64 }}\n run: |\n rm -rf tap/Formula\n mkdir -p tap/Formula\n cat > tap/Formula/alius.rb <<'TEMPLATE'\n class Alius < Formula\n desc \"LLM Agent CLI - Software Self-Evolution\"\n homepage \"https://github.com/AliusTech/alius\"\n version \"%%VERSION%%\"\n license \"MIT\"\n\n on_macos do\n if Hardware::CPU.arm?\n url \"https://github.com/AliusTech/alius/releases/download/%%RELEASE_TAG%%/alius-macos-arm64.tar.gz\"\n sha256 \"%%SHA256_ARM64%%\"\n else\n url \"https://github.com/AliusTech/alius/releases/download/%%RELEASE_TAG%%/alius-macos-x64.tar.gz\"\n sha256 \"%%SHA256_X64%%\"\n end\n end\n\n on_linux do\n url \"https://github.com/AliusTech/alius/releases/download/%%RELEASE_TAG%%/alius-linux-x64.tar.gz\"\n sha256 \"%%SHA256_LINUX_X64%%\"\n end\n\n def install\n bin.install \"alius\"\n end\n\n test do\n system bin/\"alius\", \"--version\"\n end\n end\n TEMPLATE\n\n sed -i \"s/%%VERSION%%/${VERSION}/g\" tap/Formula/alius.rb\n sed -i \"s/%%RELEASE_TAG%%/${{ needs.resolve-version.outputs.tag_name }}/g\" tap/Formula/alius.rb\n sed -i \"s/%%SHA256_ARM64%%/${SHA256_ARM64}/g\" tap/Formula/alius.rb\n sed -i \"s/%%SHA256_X64%%/${SHA256_X64}/g\" tap/Formula/alius.rb\n sed -i \"s/%%SHA256_LINUX_X64%%/${SHA256_LINUX_X64}/g\" tap/Formula/alius.rb\n\n - name: Push formula update\n if: steps.check.outputs.skip != 'true'\n run: |\n VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n cd tap\n git config user.name \"github-actions[bot]\"\n git config user.email \"github-actions[bot]@users.noreply.github.com\"\n git add Formula/alius.rb\n git diff --staged --quiet && exit 0\n git commit -m \"Update alius to ${VERSION}\"\n git push\n"}, "ed9ccbca2fdad3a89ac73177ea2f8cd0ba05cb51": {"t": 1785750733.0006082, "n": 16876, "h": "b16aca187d7a8d87", "c": "name: Release\n\non:\n push:\n branches:\n - release\n\npermissions:\n contents: write\n id-token: write\n\nconcurrency:\n group: release-${{ github.ref }}\n cancel-in-progress: false\n\nenv:\n CARGO_TERM_COLOR: always\n CARGO_INCREMENTAL: \"0\"\n\njobs:\n resolve-version:\n name: Resolve Version\n runs-on: ubuntu-latest\n outputs:\n version: ${{ steps.version.outputs.version }}\n tag_name: ${{ steps.version.outputs.tag_name }}\n steps:\n - uses: actions/checkout@v6\n with:\n fetch-depth: 0\n token: ${{ secrets.ALIUS_RELEASE_TOKEN }}\n\n - name: Read version from VERSION.txt\n id: version\n run: |\n VERSION=$(cat VERSION.txt | tr -d '[:space:]')\n echo \"version=${VERSION}\" >> $GITHUB_OUTPUT\n echo \"tag_name=v${VERSION}\" >> $GITHUB_OUTPUT\n\n - name: Check version consistency\n run: bash scripts/check-version.sh\n\n - name: Create tag\n run: |\n TAG=\"v${{ steps.version.outputs.version }}\"\n if git ls-remote origin \"refs/tags/${TAG}\" | grep -q .; then\n echo \"Tag ${TAG} already exists\"\n else\n git tag \"$TAG\"\n git push origin \"$TAG\"\n echo \"Created tag ${TAG}\"\n fi\n\n create-release:\n name: Create GitHub Release\n needs: resolve-version\n runs-on: ubuntu-latest\n permissions:\n contents: write\n steps:\n - uses: actions/checkout@v6\n\n - name: Create or verify release\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n TAG=\"${{ needs.resolve-version.outputs.tag_name }}\"\n VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n\n if gh release view \"$TAG\" >/dev/null 2>&1; then\n echo \"Release $TAG already exists, skipping creation\"\n else\n TAG_SHA=$(git ls-remote origin \"refs/tags/${TAG}\" | cut -f1)\n if [ -z \"$TAG_SHA\" ]; then\n TAG_SHA=\"$GITHUB_SHA\"\n fi\n echo \"Creating release $TAG at commit $TAG_SHA...\"\n gh release create \"$TAG\" \\\n --title \"v${VERSION}\" \\\n --generate-notes \\\n --target \"$TAG_SHA\"\n fi\n\n build-linux:\n name: Build Linux\n needs: [resolve-version, create-release]\n runs-on: ubuntu-latest\n permissions:\n contents: write\n steps:\n - uses: actions/checkout@v6\n\n - name: Install Rust\n uses: dtolnay/rust-toolchain@stable\n\n - name: Verify and fix version consistency\n run: |\n TAG_VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n CARGO_VERSION=$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[] | select(.name==\"alius-cli\") | .version')\n echo \"Tag version: $TAG_VERSION\"\n echo \"Cargo version: $CARGO_VERSION\"\n if [ \"$TAG_VERSION\" != \"$CARGO_VERSION\" ]; then\n echo \"::warning::Tag version $TAG_VERSION != Cargo version $CARGO_VERSION, auto-fixing Cargo.toml\"\n sed -i \"s/^version = \\\".*\\\"/version = \\\"${TAG_VERSION}\\\"/\" Cargo.toml\n echo \"Cargo.toml updated to version $TAG_VERSION\"\n fi\n\n - name: Cache Rust artifacts\n uses: Swatinem/rust-cache@v2\n with:\n key: ${{ needs.resolve-version.outputs.version }}-x86_64-linux\n workspaces: \". -> target\"\n cache-on-failure: true\n\n - name: Build release binary\n run: cargo build -p alius-cli --bin alius --release\n\n - name: Prepare artifacts\n run: |\n cd target/release\n tar -czvf ../../alius-linux-x64.tar.gz alius\n cd ../..\n\n - name: Upload artifact to release\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n gh release upload ${{ needs.resolve-version.outputs.tag_name }} \\\n alius-linux-x64.tar.gz --clobber\n\n build-macos-x64:\n name: Build macOS x64\n needs: [resolve-version, create-release]\n runs-on: macos-26-intel\n steps:\n - uses: actions/checkout@v6\n\n - name: Verify and fix version consistency\n run: |\n TAG_VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n CARGO_VERSION=$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[] | select(.name==\"alius-cli\") | .version')\n echo \"Tag version: $TAG_VERSION\"\n echo \"Cargo version: $CARGO_VERSION\"\n if [ \"$TAG_VERSION\" != \"$CARGO_VERSION\" ]; then\n echo \"::warning::Tag version $TAG_VERSION != Cargo version $CARGO_VERSION, auto-fixing Cargo.toml\"\n sed -i '' \"s/^version = \\\".*\\\"/version = \\\"${TAG_VERSION}\\\"/\" Cargo.toml\n echo \"Cargo.toml updated to version $TAG_VERSION\"\n fi\n\n - name: Install Rust\n uses: dtolnay/rust-toolchain@stable\n\n - name: Setup sccache\n uses: mozilla-actions/sccache-action@v0.0.9\n\n - name: Cache Rust artifacts\n uses: Swatinem/rust-cache@v2\n with:\n key: ${{ needs.resolve-version.outputs.version }}-x86_64-darwin\n cache-on-failure: true\n\n - name: Build release binary\n run: cargo build -p alius-cli --bin alius --release\n\n - name: Prepare artifact\n run: |\n cd target/release\n tar -czvf ../../alius-macos-x64.tar.gz alius\n cd ../..\n\n - name: Upload artifact to release\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n gh release upload ${{ needs.resolve-version.outputs.tag_name }} \\\n alius-macos-x64.tar.gz --clobber\n\n build-macos-arm64:\n name: Build macOS ARM64\n needs: [resolve-version, create-release]\n runs-on: macos-26\n steps:\n - uses: actions/checkout@v6\n\n - name: Verify and fix version consistency\n run: |\n TAG_VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n CARGO_VERSION=$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[] | select(.name==\"alius-cli\") | .version')\n echo \"Tag version: $TAG_VERSION\"\n echo \"Cargo version: $CARGO_VERSION\"\n if [ \"$TAG_VERSION\" != \"$CARGO_VERSION\" ]; then\n echo \"::warning::Tag version $TAG_VERSION != Cargo version $CARGO_VERSION, auto-fixing Cargo.toml\"\n sed -i '' \"s/^version = \\\".*\\\"/version = \\\"${TAG_VERSION}\\\"/\" Cargo.toml\n echo \"Cargo.toml updated to version $TAG_VERSION\"\n fi\n\n - name: Install Rust\n uses: dtolnay/rust-toolchain@stable\n\n - name: Setup sccache\n uses: mozilla-actions/sccache-action@v0.0.9\n\n - name: Cache Rust artifacts\n uses: Swatinem/rust-cache@v2\n with:\n key: ${{ needs.resolve-version.outputs.version }}-aarch64-darwin\n cache-on-failure: true\n\n - name: Build release binary\n run: cargo build -p alius-cli --bin alius --release\n\n - name: Prepare artifact\n run: |\n cd target/release\n tar -czvf ../../alius-macos-arm64.tar.gz alius\n cd ../..\n\n - name: Upload artifact to release\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n gh release upload ${{ needs.resolve-version.outputs.tag_name }} \\\n alius-macos-arm64.tar.gz --clobber\n\n build-windows:\n name: Build Windows\n needs: [resolve-version, create-release]\n runs-on: windows-2025-vs2026\n steps:\n - uses: actions/checkout@v6\n\n - name: Verify and fix version consistency\n shell: bash\n run: |\n TAG_VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n CARGO_VERSION=$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[] | select(.name==\"alius-cli\") | .version')\n echo \"Tag version: $TAG_VERSION\"\n echo \"Cargo version: $CARGO_VERSION\"\n if [ \"$TAG_VERSION\" != \"$CARGO_VERSION\" ]; then\n echo \"::warning::Tag version $TAG_VERSION != Cargo version $CARGO_VERSION, auto-fixing Cargo.toml\"\n sed -i \"s/^version = \\\".*\\\"/version = \\\"${TAG_VERSION}\\\"/\" Cargo.toml\n echo \"Cargo.toml updated to version $TAG_VERSION\"\n fi\n\n - name: Install Rust\n uses: dtolnay/rust-toolchain@stable\n\n - name: Setup sccache\n uses: mozilla-actions/sccache-action@v0.0.9\n\n - name: Cache Rust artifacts\n uses: Swatinem/rust-cache@v2\n with:\n key: ${{ needs.resolve-version.outputs.version }}-x86_64-windows\n cache-on-failure: true\n\n - name: Build release binary\n run: cargo build -p alius-cli --bin alius --release\n\n - name: Prepare artifact\n run: |\n cd target/release\n 7z a ../../alius-windows-x64.zip alius.exe\n cd ../..\n\n - name: Upload artifact to release\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n gh release upload ${{ needs.resolve-version.outputs.tag_name }} \\\n alius-windows-x64.zip --clobber\n\n verify-assets:\n name: Verify Release Assets\n needs:\n - resolve-version\n - build-linux\n - build-macos-x64\n - build-macos-arm64\n - build-windows\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v6\n\n - name: Verify all assets exist\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n TAG: ${{ needs.resolve-version.outputs.tag_name }}\n run: |\n echo \"Verifying assets for release $TAG...\"\n\n ASSETS=$(gh release view \"$TAG\" --json assets --jq '.assets[].name')\n echo \"Found assets:\"\n echo \"$ASSETS\"\n\n REQUIRED=(\n \"alius-linux-x64.tar.gz\"\n \"alius-macos-x64.tar.gz\"\n \"alius-macos-arm64.tar.gz\"\n \"alius-windows-x64.zip\"\n )\n\n MISSING=0\n for asset in \"${REQUIRED[@]}\"; do\n if ! echo \"$ASSETS\" | grep -q \"$asset\"; then\n echo \"::error::Missing required asset: $asset\"\n MISSING=1\n fi\n done\n\n if [ $MISSING -ne 0 ]; then\n echo \"::error::Some required assets are missing\"\n exit 1\n fi\n\n echo \"All required assets verified\"\n\n publish-npm:\n name: Publish npm\n needs:\n - resolve-version\n - verify-assets\n runs-on: ubuntu-latest\n permissions:\n contents: read\n id-token: write\n steps:\n - uses: actions/checkout@v6\n\n - name: Setup Node.js\n uses: actions/setup-node@v6\n with:\n node-version: '24'\n registry-url: 'https://registry.npmjs.org'\n package-manager-cache: false\n\n - name: Verify Trusted Publishing prerequisites\n run: |\n node --version\n npm --version\n node - <<'NODE'\n const { execSync } = require('child_process');\n\n function assertAtLeast(name, actual, minimum) {\n const got = actual.replace(/^v/, '').split('.').map(Number);\n const min = minimum.split('.').map(Number);\n for (let i = 0; i < min.length; i += 1) {\n if ((got[i] || 0) > min[i]) return;\n if ((got[i] || 0) < min[i]) {\n throw new Error(`${name} ${actual} is below required ${minimum}`);\n }\n }\n }\n\n assertAtLeast('Node.js', process.version, '22.14.0');\n assertAtLeast('npm', execSync('npm --version', { encoding: 'utf8' }).trim(), '11.5.1');\n NODE\n\n - name: Prepare npm packages\n run: |\n VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n RELEASE_TAG=\"${{ needs.resolve-version.outputs.tag_name }}\"\n node npm-packages/generate-packages.js \"$VERSION\" \"$RELEASE_TAG\"\n node npm-packages/verify-release-version.js \"$VERSION\" \"$RELEASE_TAG\"\n\n - name: Publish platform packages\n run: |\n VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n\n for pkg in alius-darwin-x64 alius-darwin-arm64 alius-linux-x64 alius-win32-x64; do\n if npm view \"@alius-tech/$pkg@$VERSION\" version >/dev/null 2>&1; then\n echo \"Skipping @alius-tech/$pkg@$VERSION (already published)\"\n continue\n fi\n echo \"Publishing @alius-tech/$pkg@$VERSION...\"\n cd \"npm-packages/$pkg\"\n npm publish --access public --provenance --tag latest\n cd ../..\n done\n\n - name: Publish main package\n run: |\n VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n\n if npm view \"@alius-tech/alius@$VERSION\" version >/dev/null 2>&1; then\n echo \"Skipping @alius-tech/alius@$VERSION (already published)\"\n exit 0\n fi\n\n cd npm-packages/alius\n npm publish --access public --provenance --tag latest\n\n update-homebrew:\n name: Update Homebrew\n needs:\n - resolve-version\n - verify-assets\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v6\n\n - name: Download release artifacts\n run: |\n RELEASE_TAG=\"${{ needs.resolve-version.outputs.tag_name }}\"\n BASE_URL=\"https://github.com/AliusTech/alius/releases/download/${RELEASE_TAG}\"\n mkdir -p artifacts\n\n curl -fsSL -o artifacts/alius-macos-arm64.tar.gz \"${BASE_URL}/alius-macos-arm64.tar.gz\"\n curl -fsSL -o artifacts/alius-macos-x64.tar.gz \"${BASE_URL}/alius-macos-x64.tar.gz\"\n curl -fsSL -o artifacts/alius-linux-x64.tar.gz \"${BASE_URL}/alius-linux-x64.tar.gz\"\n\n - name: Compute SHA256 checksums\n id: sha256\n run: |\n echo \"arm64=$(sha256sum artifacts/alius-macos-arm64.tar.gz | cut -d' ' -f1)\" >> $GITHUB_OUTPUT\n echo \"x64=$(sha256sum artifacts/alius-macos-x64.tar.gz | cut -d' ' -f1)\" >> $GITHUB_OUTPUT\n echo \"linux_x64=$(sha256sum artifacts/alius-linux-x64.tar.gz | cut -d' ' -f1)\" >> $GITHUB_OUTPUT\n\n - name: Checkout homebrew tap\n uses: actions/checkout@v6\n with:\n repository: AliusTech/homebrew-tap\n token: ${{ secrets.TAP_GITHUB_TOKEN }}\n path: tap\n\n - name: Check if version already published\n id: check\n run: |\n VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n FORMULA=\"tap/Formula/alius.rb\"\n if [ -f \"$FORMULA\" ] && grep -q \"version \\\"${VERSION}\\\"\" \"$FORMULA\"; then\n echo \"Homebrew formula already at version ${VERSION}, skipping\"\n echo \"skip=true\" >> $GITHUB_OUTPUT\n else\n echo \"skip=false\" >> $GITHUB_OUTPUT\n fi\n\n - name: Update formula\n if: steps.check.outputs.skip != 'true'\n env:\n VERSION: ${{ needs.resolve-version.outputs.version }}\n SHA256_ARM64: ${{ steps.sha256.outputs.arm64 }}\n SHA256_X64: ${{ steps.sha256.outputs.x64 }}\n SHA256_LINUX_X64: ${{ steps.sha256.outputs.linux_x64 }}\n run: |\n rm -rf tap/Formula\n mkdir -p tap/Formula\n cat > tap/Formula/alius.rb <<'TEMPLATE'\n class Alius < Formula\n desc \"LLM Agent CLI - Software Self-Evolution\"\n homepage \"https://github.com/AliusTech/alius\"\n version \"%%VERSION%%\"\n license \"MIT\"\n\n on_macos do\n if Hardware::CPU.arm?\n url \"https://github.com/AliusTech/alius/releases/download/%%RELEASE_TAG%%/alius-macos-arm64.tar.gz\"\n sha256 \"%%SHA256_ARM64%%\"\n else\n url \"https://github.com/AliusTech/alius/releases/download/%%RELEASE_TAG%%/alius-macos-x64.tar.gz\"\n sha256 \"%%SHA256_X64%%\"\n end\n end\n\n on_linux do\n url \"https://github.com/AliusTech/alius/releases/download/%%RELEASE_TAG%%/alius-linux-x64.tar.gz\"\n sha256 \"%%SHA256_LINUX_X64%%\"\n end\n\n def install\n bin.install \"alius\"\n end\n\n test do\n system bin/\"alius\", \"--version\"\n end\n end\n TEMPLATE\n\n sed -i \"s/%%VERSION%%/${VERSION}/g\" tap/Formula/alius.rb\n sed -i \"s/%%RELEASE_TAG%%/${{ needs.resolve-version.outputs.tag_name }}/g\" tap/Formula/alius.rb\n sed -i \"s/%%SHA256_ARM64%%/${SHA256_ARM64}/g\" tap/Formula/alius.rb\n sed -i \"s/%%SHA256_X64%%/${SHA256_X64}/g\" tap/Formula/alius.rb\n sed -i \"s/%%SHA256_LINUX_X64%%/${SHA256_LINUX_X64}/g\" tap/Formula/alius.rb\n\n - name: Push formula update\n if: steps.check.outputs.skip != 'true'\n run: |\n VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n cd tap\n git config user.name \"github-actions[bot]\"\n git config user.email \"github-actions[bot]@users.noreply.github.com\"\n git add Formula/alius.rb\n git diff --staged --quiet && exit 0\n git commit -m \"Update alius to ${VERSION}\"\n git push\n"}}}}