Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions crates/rustyclaw-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use rustyclaw_core::skills::SkillManager;
#[cfg(feature = "tui")]
use rustyclaw_tui::app::App;
#[cfg(feature = "tui")]
use rustyclaw_tui::onboard::run_onboard_wizard;
use rustyclaw_tui::onboard::{run_onboard_wizard, OnboardArgs as TuiOnboardArgs};
use std::path::PathBuf;
use tokio_tungstenite::tungstenite::Message;
use url::Url;
Expand Down Expand Up @@ -604,7 +604,16 @@ async fn main() -> Result<()> {
#[cfg(feature = "tui")]
{
let mut secrets = open_secrets(&config)?;
run_onboard_wizard(&mut config, &mut secrets, false, args.non_interactive)?;
let tui_args = TuiOnboardArgs {
openrouter_api_key: None,
anthropic_api_key: None,
openai_api_key: None,
gemini_api_key: None,
xai_api_key: None,
reset: false,
non_interactive: args.non_interactive,
};
run_onboard_wizard(&mut config, &mut secrets, Some(tui_args))?;
// Optional agent setup step
let ws_dir = config.workspace_dir();
match rustyclaw_core::tools::agent_setup::exec_agent_setup(
Expand Down Expand Up @@ -659,7 +668,16 @@ async fn main() -> Result<()> {
#[cfg(feature = "tui")]
{
let mut secrets = open_secrets(&config)?;
run_onboard_wizard(&mut config, &mut secrets, _args.reset, _args.non_interactive)?;
let tui_args = TuiOnboardArgs {
openrouter_api_key: _args.openrouter_api_key.clone(),
anthropic_api_key: _args.anthropic_api_key.clone(),
openai_api_key: _args.openai_api_key.clone(),
gemini_api_key: _args.gemini_api_key.clone(),
xai_api_key: _args.xai_api_key.clone(),
reset: _args.reset,
non_interactive: _args.non_interactive,
};
run_onboard_wizard(&mut config, &mut secrets, Some(tui_args))?;
// Optional agent setup step
let ws_dir = config.workspace_dir();
match rustyclaw_core::tools::agent_setup::exec_agent_setup(
Expand Down Expand Up @@ -709,7 +727,7 @@ async fn main() -> Result<()> {
#[cfg(feature = "tui")]
{
let mut secrets = open_secrets(&config)?;
run_onboard_wizard(&mut config, &mut secrets, false, false)?;
run_onboard_wizard(&mut config, &mut secrets, None)?;
}
#[cfg(not(feature = "tui"))]
{
Expand Down
169 changes: 122 additions & 47 deletions crates/rustyclaw-tui/src/onboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,27 @@ use rustyclaw_core::theme as t;

// ── Public entry point ──────────────────────────────────────────────────────

/// Onboard arguments from CLI (optional).
pub struct OnboardArgs {
pub openrouter_api_key: Option<String>,
// Add other API key fields as needed
pub anthropic_api_key: Option<String>,
pub openai_api_key: Option<String>,
pub gemini_api_key: Option<String>,
pub xai_api_key: Option<String>,
pub reset: bool,
pub non_interactive: bool,
}
Comment on lines +21 to +30
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 opencode_api_key CLI flag silently ignored — not forwarded to TUI OnboardArgs

The CLI's OnboardArgs declares opencode_api_key (crates/rustyclaw-cli/src/main.rs:228-229) and accepts it via --opencode-api-key or the OPENCODE_API_KEY env var. However, when constructing TuiOnboardArgs at lines 662-669, opencode_api_key is never forwarded. The TUI's OnboardArgs struct (crates/rustyclaw-tui/src/onboard.rs:21-29) also lacks this field entirely. Additionally, the provider auto-selection logic (onboard.rs:339-358) and the API key matching logic (onboard.rs:409-415) have no case for "opencode". This means users who pass --opencode-api-key or set OPENCODE_API_KEY will have their key silently ignored — the OpenCode Zen provider won't be auto-selected and the key won't be stored.

Prompt for agents
Three changes are needed:

1. In crates/rustyclaw-tui/src/onboard.rs, add an opencode_api_key field to the OnboardArgs struct at line 27 (before xai_api_key):
   pub opencode_api_key: Option<String>,

2. In the same file, add an opencode auto-selection branch in the provider selection block (around line 355-358), e.g.:
   } else if args.opencode_api_key.is_some() {
       println!("  {}", t::icon_ok("Auto-selecting OpenCode Zen provider based on --opencode-api-key flag"));
       PROVIDERS.iter().find(|p| p.id == "opencode").unwrap()

3. In the same file, add an opencode case to the API key matching logic (around lines 409-415):
       "opencode" => args.opencode_api_key.as_ref(),

4. In crates/rustyclaw-cli/src/main.rs, forward the key when constructing TuiOnboardArgs (around line 667):
       opencode_api_key: _args.opencode_api_key.clone(),
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


/// Run the interactive onboarding wizard, mutating `config` in place and
/// storing secrets. Returns `true` if the user completed onboarding.
pub fn run_onboard_wizard(
config: &mut Config,
secrets: &mut SecretsManager,
reset: bool,
non_interactive: bool,
args: Option<OnboardArgs>,
) -> Result<bool> {
let reset = args.as_ref().map(|a| a.reset).unwrap_or(false);
let non_interactive = args.as_ref().map(|a| a.non_interactive).unwrap_or(false);
let stdin = io::stdin();
let mut reader = stdin.lock();

Expand Down Expand Up @@ -325,18 +338,60 @@ pub fn run_onboard_wizard(
}

// ── 2. Select model provider ───────────────────────────────────
let provider_names: Vec<&str> = PROVIDERS.iter().map(|p| p.display).collect();
let provider = match arrow_select(&provider_names, "Select a model provider:")? {
Some(idx) => &PROVIDERS[idx],
None => {
println!(" {}", t::warn("Cancelled."));
// Save any config changes made during vault setup before returning.
config
.ensure_dirs()
.context("Failed to create directory structure")?;
config.save(None)?;
println!(" {}", t::muted("Partial config saved."));
return Ok(false);
let provider = if let Some(ref args) = args {
// Check for auto-selection based on API key flags
if args.openrouter_api_key.is_some() {
// Auto-select OpenRouter
println!(" {}", t::icon_ok("Auto-selecting OpenRouter provider based on --openrouter-api-key flag"));
PROVIDERS.iter().find(|p| p.id == "openrouter").unwrap()
} else if args.anthropic_api_key.is_some() {
// Auto-select Anthropic
println!(" {}", t::icon_ok("Auto-selecting Anthropic provider based on --anthropic-api-key flag"));
PROVIDERS.iter().find(|p| p.id == "anthropic").unwrap()
} else if args.openai_api_key.is_some() {
// Auto-select OpenAI
println!(" {}", t::icon_ok("Auto-selecting OpenAI provider based on --openai-api-key flag"));
PROVIDERS.iter().find(|p| p.id == "openai").unwrap()
} else if args.gemini_api_key.is_some() {
// Auto-select Google
println!(" {}", t::icon_ok("Auto-selecting Google provider based on --gemini-api-key flag"));
PROVIDERS.iter().find(|p| p.id == "google").unwrap()
} else if args.xai_api_key.is_some() {
// Auto-select xAI
println!(" {}", t::icon_ok("Auto-selecting xAI provider based on --xai-api-key flag"));
PROVIDERS.iter().find(|p| p.id == "xai").unwrap()
} else {
// No API key provided, show interactive selection
let provider_names: Vec<&str> = PROVIDERS.iter().map(|p| p.display).collect();
match arrow_select(&provider_names, "Select a model provider:")? {
Some(idx) => &PROVIDERS[idx],
None => {
println!(" {}", t::warn("Cancelled."));
// Save any config changes made during vault setup before returning.
config
.ensure_dirs()
.context("Failed to create directory structure")?;
config.save(None)?;
println!(" {}", t::muted("Partial config saved."));
return Ok(false);
}
}
}
} else {
// No args provided, show interactive selection
let provider_names: Vec<&str> = PROVIDERS.iter().map(|p| p.display).collect();
match arrow_select(&provider_names, "Select a model provider:")? {
Some(idx) => &PROVIDERS[idx],
None => {
println!(" {}", t::warn("Cancelled."));
// Save any config changes made during vault setup before returning.
config
.ensure_dirs()
.context("Failed to create directory structure")?;
config.save(None)?;
println!(" {}", t::muted("Partial config saved."));
return Ok(false);
}
}
};

Expand All @@ -353,48 +408,68 @@ pub fn run_onboard_wizard(
if let Some(secret_key) = provider.secret_key {
match provider.auth_method {
AuthMethod::ApiKey => {
// Standard API key authentication
let existing = secrets.get_secret(secret_key, true)?;
if existing.is_some() {
let reuse = prompt_line(
&mut reader,
&format!(
"{} ",
t::accent(&format!(
"An API key for {} is already stored. Keep it? [Y/n]:",
provider.display
))
),
)?;
if reuse.trim().eq_ignore_ascii_case("n") {
let key = prompt_secret(
// Check if API key was provided via CLI args first
let provided_key = if let Some(ref args) = args {
match provider.id {
"openrouter" => args.openrouter_api_key.as_ref(),
"anthropic" => args.anthropic_api_key.as_ref(),
"openai" => args.openai_api_key.as_ref(),
"google" => args.gemini_api_key.as_ref(),
"xai" => args.xai_api_key.as_ref(),
_ => None,
}
} else {
None
};

if let Some(key) = provided_key {
// Store the provided API key
secrets.store_secret(secret_key, key)?;
println!(" {}", t::icon_ok("API key stored securely."));
} else {
// Standard API key authentication flow
let existing = secrets.get_secret(secret_key, true)?;
if existing.is_some() {
let reuse = prompt_line(
&mut reader,
&format!("{} ", t::accent("Enter API key:")),
&format!(
"{} ",
t::accent(&format!(
"An API key for {} is already stored. Keep it? [Y/n]:",
provider.display
))
),
)?;
if reuse.trim().eq_ignore_ascii_case("n") {
let key = prompt_secret(
&mut reader,
&format!("{} ", t::accent("Enter API key:")),
)?;
if key.trim().is_empty() {
println!(
" {}",
t::icon_warn("No key entered — keeping existing key.")
);
} else {
secrets.store_secret(secret_key, key.trim())?;
println!(" {}", t::icon_ok("API key updated."));
}
} else {
println!(" {}", t::icon_ok("Keeping existing API key."));
}
} else {
let key =
prompt_secret(&mut reader, &format!("{} ", t::accent("Enter API key:")))?;
if key.trim().is_empty() {
println!(
" {}",
t::icon_warn("No key entered — keeping existing key.")
t::icon_warn("No key entered — you can add one later with:")
);
println!(" {}", t::accent_bright("rustyclaw onboard"));
} else {
secrets.store_secret(secret_key, key.trim())?;
println!(" {}", t::icon_ok("API key updated."));
println!(" {}", t::icon_ok("API key stored securely."));
}
} else {
println!(" {}", t::icon_ok("Keeping existing API key."));
}
} else {
let key =
prompt_secret(&mut reader, &format!("{} ", t::accent("Enter API key:")))?;
if key.trim().is_empty() {
println!(
" {}",
t::icon_warn("No key entered — you can add one later with:")
);
println!(" {}", t::accent_bright("rustyclaw onboard"));
} else {
secrets.store_secret(secret_key, key.trim())?;
println!(" {}", t::icon_ok("API key stored securely."));
}
}
}
Expand Down
Loading