1use crate::cloud_client::ServerClient;
2use crate::models::ServerConnection;
3use crate::{config, core, git_context};
4use anyhow::{bail, Context, Result};
5use serde_json::json;
6use std::io::{self, IsTerminal, Write};
7
8pub fn cmd_connect(args: &[String]) {
9 if has_help_flag(args) {
10 println!("Usage: nebu-ctx connect [--endpoint <url>] [--token <token>]");
11 return;
12 }
13 if let Err(error) = connect_cloud(args) {
14 eprintln!("{error}");
15 std::process::exit(1);
16 }
17}
18
19pub fn cmd_disconnect() {
20 if let Err(error) = disconnect_cloud() {
21 eprintln!("{error}");
22 std::process::exit(1);
23 }
24}
25
26pub fn cmd_bind() {
27 if let Err(error) = bind_current_project() {
28 eprintln!("{error}");
29 std::process::exit(1);
30 }
31}
32
33fn connect_cloud(command_args: &[String]) -> Result<()> {
34 let saved_connection = config::load_connection().ok().flatten();
35 let endpoint = match option_value(command_args, &["--endpoint", "-e", "--url"]) {
36 Some(value) => value,
37 None => match saved_connection.as_ref() {
38 Some(connection) => connection.endpoint.clone(),
39 None => prompt_required_value("Cloud URL", None)?,
40 },
41 };
42 let token = match option_value(command_args, &["--token", "-t"]) {
43 Some(value) => value,
44 None => prompt_required_secret("Cloud token")?,
45 };
46
47 let (connection, client) = validate_and_save_connection(&endpoint, &token)?;
48 let health = client.health()?;
49 output_json(json!({
50 "connected": true,
51 "endpoint": connection.endpoint,
52 "health": health,
53 }))
54}
55
56fn bind_current_project() -> Result<()> {
57 let client = load_or_prompt_cloud_client()?;
58 let project_context = git_context::discover_project_context(
59 &std::env::current_dir().context("failed to read current directory")?,
60 );
61 output_json(serde_json::to_value(client.resolve_project(&project_context)?)?)
62}
63
64fn disconnect_cloud() -> Result<()> {
65 config::clear_connection()?;
66 output_json(json!({ "disconnected": true }))
67}
68
69fn load_or_prompt_cloud_client() -> Result<ServerClient> {
70 if let Ok(client) = ServerClient::load() {
71 return Ok(client);
72 }
73
74 if !io::stdin().is_terminal() {
75 bail!("No cloud connection saved. Run `nebu-ctx connect --endpoint <url> --token <token>`.");
76 }
77
78 let endpoint = prompt_required_value("Cloud URL", None)?;
79 let token = prompt_required_secret("Cloud token")?;
80 let (_, client) = validate_and_save_connection(&endpoint, &token)?;
81 Ok(client)
82}
83
84fn validate_and_save_connection(endpoint: &str, token: &str) -> Result<(ServerConnection, ServerClient)> {
85 let connection = ServerConnection {
86 endpoint: config::normalize_server_endpoint(endpoint),
87 token: token.trim().to_string(),
88 };
89 let client = ServerClient::new(connection.clone());
90 client.health()?;
91 let saved_connection = config::save_connection(&connection.endpoint, &connection.token)?;
92 Ok((saved_connection, client))
93}
94
95fn prompt_required_value(label: &str, default_value: Option<&str>) -> Result<String> {
96 loop {
97 print!("{label}");
98 if let Some(default_value) = default_value {
99 print!(" [{default_value}]");
100 }
101 print!(": ");
102 io::stdout().flush().context("failed to flush prompt")?;
103
104 let mut input = String::new();
105 io::stdin()
106 .read_line(&mut input)
107 .context("failed to read terminal input")?;
108 let trimmed = input.trim();
109 if !trimmed.is_empty() {
110 return Ok(trimmed.to_string());
111 }
112
113 if let Some(default_value) = default_value {
114 return Ok(default_value.to_string());
115 }
116 }
117}
118
119fn prompt_required_secret(label: &str) -> Result<String> {
120 loop {
121 let value = rpassword::prompt_password(format!("{label}: "))
122 .context("failed to read token from terminal")?;
123 if !value.trim().is_empty() {
124 return Ok(value);
125 }
126 }
127}
128
129fn option_value(command_args: &[String], flags: &[&str]) -> Option<String> {
130 let mut index = 0;
131 while index < command_args.len() {
132 if flags.contains(&command_args[index].as_str()) {
133 return command_args.get(index + 1).cloned();
134 }
135
136 index += 1;
137 }
138
139 None
140}
141
142fn has_help_flag(command_args: &[String]) -> bool {
143 command_args
144 .iter()
145 .any(|argument| matches!(argument.as_str(), "--help" | "-h" | "help"))
146}
147
148fn output_json(value: serde_json::Value) -> Result<()> {
149 println!("{}", serde_json::to_string_pretty(&value)?);
150 Ok(())
151}
152
153pub fn cmd_gotchas(args: &[String]) {
154 let action = args.first().map(|value| value.as_str()).unwrap_or("list");
155 let project_root = std::env::current_dir()
156 .map(|path| path.to_string_lossy().to_string())
157 .unwrap_or_else(|_| ".".to_string());
158
159 match action {
160 "list" | "ls" => {
161 let store = core::gotcha_tracker::GotchaStore::load(&project_root);
162 println!("{}", store.format_list());
163 }
164 "clear" => {
165 let mut store = core::gotcha_tracker::GotchaStore::load(&project_root);
166 let count = store.gotchas.len();
167 store.clear();
168 let _ = store.save(&project_root);
169 println!("Cleared {count} gotchas.");
170 }
171 "export" => {
172 let store = core::gotcha_tracker::GotchaStore::load(&project_root);
173 match serde_json::to_string_pretty(&store.gotchas) {
174 Ok(json) => println!("{json}"),
175 Err(error) => eprintln!("Export failed: {error}"),
176 }
177 }
178 "stats" => {
179 let store = core::gotcha_tracker::GotchaStore::load(&project_root);
180 println!("Bug Memory Stats:");
181 println!(" Active gotchas: {}", store.gotchas.len());
182 println!(" Errors detected: {}", store.stats.total_errors_detected);
183 println!(" Fixes correlated: {}", store.stats.total_fixes_correlated);
184 println!(" Bugs prevented: {}", store.stats.total_prevented);
185 println!(" Promoted to knowledge: {}", store.stats.gotchas_promoted);
186 println!(" Decayed/archived: {}", store.stats.gotchas_decayed);
187 println!(" Session logs: {}", store.error_log.len());
188 }
189 _ => {
190 println!("Usage: nebu-ctx gotchas [list|clear|export|stats]");
191 }
192 }
193}
194
195pub fn cmd_buddy(args: &[String]) {
196 let cfg = core::config::Config::load();
197 if !cfg.buddy_enabled {
198 println!("Buddy is disabled. Enable with: nebu-ctx config buddy_enabled true");
199 return;
200 }
201
202 let action = args.first().map(|value| value.as_str()).unwrap_or("show");
203 let buddy = core::buddy::BuddyState::compute();
204 let theme = core::theme::load_theme(&cfg.theme);
205
206 match action {
207 "show" | "status" | "stats" => {
208 println!("{}", core::buddy::format_buddy_full(&buddy, &theme));
209 }
210 "ascii" => {
211 for line in &buddy.ascii_art {
212 println!(" {line}");
213 }
214 }
215 "json" => match serde_json::to_string_pretty(&buddy) {
216 Ok(json) => println!("{json}"),
217 Err(error) => eprintln!("JSON error: {error}"),
218 },
219 _ => {
220 println!("Usage: nebu-ctx buddy [show|stats|ascii|json]");
221 }
222 }
223}
224