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
8fn removed_cloud_command(command: &str) -> ! {
9 eprintln!(
10 "`nebu-ctx {command}` is no longer available in the Rust client. Use `nebu-ctx cloud connect --endpoint <url> --token <token>` against your NebuCtx server."
11 );
12 std::process::exit(1);
13}
14
15pub fn cmd_login(_args: &[String]) {
16 removed_cloud_command("login");
17}
18
19pub fn cmd_forgot_password(_args: &[String]) {
20 removed_cloud_command("forgot-password");
21}
22
23pub fn cmd_register(_args: &[String]) {
24 removed_cloud_command("register");
25}
26
27pub fn cmd_sync() {
28 if let Err(error) = sync_current_checkout() {
29 eprintln!("{error}");
30 std::process::exit(1);
31 }
32}
33
34fn sync_current_checkout() -> Result<()> {
35 let client = load_or_prompt_cloud_client()?;
36
37 let cwd = std::env::current_dir().context("failed to read current directory")?;
38 let project_context = git_context::discover_project_context(&cwd);
39
40 let response = client
41 .resolve_project(&project_context)
42 .context("failed to sync with cloud")?;
43
44 let binding = &project_context.checkout_binding;
45 output_json(json!({
46 "synced": true,
47 "project_id": response.project.project_id,
48 "slug": response.project.slug,
49 "checkout_bound": response.checkout_bound,
50 "branch": binding.branch,
51 "commit": binding.last_commit,
52 "local_root": binding.local_root,
53 "endpoint": client.endpoint(),
54 }))
55}
56
57pub fn cmd_contribute() {
58 removed_cloud_command("contribute");
59}
60
61pub fn cmd_cloud(args: &[String]) {
62 let action = args.first().map(|value| value.as_str()).unwrap_or("help");
63
64 match action {
65 "connect" => {
66 if let Err(error) = connect_cloud(&args[1..]) {
67 eprintln!("{error}");
68 std::process::exit(1);
69 }
70 }
71 "bind" => {
72 if let Err(error) = bind_current_project() {
73 eprintln!("{error}");
74 std::process::exit(1);
75 }
76 }
77 "sync" => {
78 cmd_sync();
79 }
80 "disconnect" => {
81 if let Err(error) = disconnect_cloud() {
82 eprintln!("{error}");
83 std::process::exit(1);
84 }
85 }
86 "status" => {
87 if let Err(error) = show_cloud_status() {
88 eprintln!("{error}");
89 std::process::exit(1);
90 }
91 }
92 _ => {
93 println!("Usage: nebu-ctx cloud <command>");
94 println!(" connect - Save and validate a cloud endpoint + token");
95 println!(" status - Show cloud connection status");
96 println!(" bind - Bind the current checkout to a canonical project");
97 println!(" sync - Sync current checkout state (branch, commit) to the cloud");
98 println!(" disconnect - Remove the saved cloud connection");
99 }
100 }
101}
102
103fn connect_cloud(command_args: &[String]) -> Result<()> {
104 if has_help_flag(command_args) {
105 println!("Usage: nebu-ctx cloud connect [--endpoint <url>] [--token <token>]");
106 return Ok(());
107 }
108
109 let saved_connection = config::load_connection().ok().flatten();
110 let endpoint = match option_value(command_args, &["--endpoint", "-e", "--url"]) {
111 Some(value) => value,
112 None => match saved_connection.as_ref() {
113 Some(connection) => connection.endpoint.clone(),
114 None => prompt_required_value("Cloud URL", None)?,
115 },
116 };
117 let token = match option_value(command_args, &["--token", "-t"]) {
118 Some(value) => value,
119 None => prompt_required_secret("Cloud token")?,
120 };
121
122 let (connection, client) = validate_and_save_connection(&endpoint, &token)?;
123 let health = client.health()?;
124 output_json(json!({
125 "connected": true,
126 "endpoint": connection.endpoint,
127 "health": health,
128 }))
129}
130
131fn show_cloud_status() -> Result<()> {
132 let client = load_or_prompt_cloud_client()?;
133 let health = client.health()?;
134 output_json(json!({
135 "saved": true,
136 "endpoint": client.endpoint(),
137 "health": health,
138 }))
139}
140
141fn bind_current_project() -> Result<()> {
142 let client = load_or_prompt_cloud_client()?;
143 let project_context = git_context::discover_project_context(
144 &std::env::current_dir().context("failed to read current directory")?,
145 );
146 output_json(serde_json::to_value(client.resolve_project(&project_context)?)?)
147}
148
149fn disconnect_cloud() -> Result<()> {
150 config::clear_connection()?;
151 output_json(json!({ "disconnected": true }))
152}
153
154fn load_or_prompt_cloud_client() -> Result<ServerClient> {
155 if let Ok(client) = ServerClient::load() {
156 return Ok(client);
157 }
158
159 if !io::stdin().is_terminal() {
160 bail!("No cloud connection saved. Run `nebu-ctx cloud connect --endpoint <url> --token <token>`." );
161 }
162
163 let endpoint = prompt_required_value("Cloud URL", None)?;
164 let token = prompt_required_secret("Cloud token")?;
165 let (_, client) = validate_and_save_connection(&endpoint, &token)?;
166 Ok(client)
167}
168
169fn validate_and_save_connection(endpoint: &str, token: &str) -> Result<(ServerConnection, ServerClient)> {
170 let connection = ServerConnection {
171 endpoint: config::normalize_server_endpoint(endpoint),
172 token: token.trim().to_string(),
173 };
174 let client = ServerClient::new(connection.clone());
175 client.health()?;
176 let saved_connection = config::save_connection(&connection.endpoint, &connection.token)?;
177 Ok((saved_connection, client))
178}
179
180fn prompt_required_value(label: &str, default_value: Option<&str>) -> Result<String> {
181 loop {
182 print!("{label}");
183 if let Some(default_value) = default_value {
184 print!(" [{default_value}]");
185 }
186 print!(": ");
187 io::stdout().flush().context("failed to flush prompt")?;
188
189 let mut input = String::new();
190 io::stdin()
191 .read_line(&mut input)
192 .context("failed to read terminal input")?;
193 let trimmed = input.trim();
194 if !trimmed.is_empty() {
195 return Ok(trimmed.to_string());
196 }
197
198 if let Some(default_value) = default_value {
199 return Ok(default_value.to_string());
200 }
201 }
202}
203
204fn prompt_required_secret(label: &str) -> Result<String> {
205 loop {
206 let value = rpassword::prompt_password(format!("{label}: "))
207 .context("failed to read token from terminal")?;
208 if !value.trim().is_empty() {
209 return Ok(value);
210 }
211 }
212}
213
214fn option_value(command_args: &[String], flags: &[&str]) -> Option<String> {
215 let mut index = 0;
216 while index < command_args.len() {
217 if flags.contains(&command_args[index].as_str()) {
218 return command_args.get(index + 1).cloned();
219 }
220
221 index += 1;
222 }
223
224 None
225}
226
227fn has_help_flag(command_args: &[String]) -> bool {
228 command_args
229 .iter()
230 .any(|argument| matches!(argument.as_str(), "--help" | "-h" | "help"))
231}
232
233fn output_json(value: serde_json::Value) -> Result<()> {
234 println!("{}", serde_json::to_string_pretty(&value)?);
235 Ok(())
236}
237
238pub fn cmd_gotchas(args: &[String]) {
239 let action = args.first().map(|value| value.as_str()).unwrap_or("list");
240 let project_root = std::env::current_dir()
241 .map(|path| path.to_string_lossy().to_string())
242 .unwrap_or_else(|_| ".".to_string());
243
244 match action {
245 "list" | "ls" => {
246 let store = core::gotcha_tracker::GotchaStore::load(&project_root);
247 println!("{}", store.format_list());
248 }
249 "clear" => {
250 let mut store = core::gotcha_tracker::GotchaStore::load(&project_root);
251 let count = store.gotchas.len();
252 store.clear();
253 let _ = store.save(&project_root);
254 println!("Cleared {count} gotchas.");
255 }
256 "export" => {
257 let store = core::gotcha_tracker::GotchaStore::load(&project_root);
258 match serde_json::to_string_pretty(&store.gotchas) {
259 Ok(json) => println!("{json}"),
260 Err(error) => eprintln!("Export failed: {error}"),
261 }
262 }
263 "stats" => {
264 let store = core::gotcha_tracker::GotchaStore::load(&project_root);
265 println!("Bug Memory Stats:");
266 println!(" Active gotchas: {}", store.gotchas.len());
267 println!(" Errors detected: {}", store.stats.total_errors_detected);
268 println!(" Fixes correlated: {}", store.stats.total_fixes_correlated);
269 println!(" Bugs prevented: {}", store.stats.total_prevented);
270 println!(" Promoted to knowledge: {}", store.stats.gotchas_promoted);
271 println!(" Decayed/archived: {}", store.stats.gotchas_decayed);
272 println!(" Session logs: {}", store.error_log.len());
273 }
274 _ => {
275 println!("Usage: nebu-ctx gotchas [list|clear|export|stats]");
276 }
277 }
278}
279
280pub fn cmd_buddy(args: &[String]) {
281 let cfg = core::config::Config::load();
282 if !cfg.buddy_enabled {
283 println!("Buddy is disabled. Enable with: nebu-ctx config buddy_enabled true");
284 return;
285 }
286
287 let action = args.first().map(|value| value.as_str()).unwrap_or("show");
288 let buddy = core::buddy::BuddyState::compute();
289 let theme = core::theme::load_theme(&cfg.theme);
290
291 match action {
292 "show" | "status" | "stats" => {
293 println!("{}", core::buddy::format_buddy_full(&buddy, &theme));
294 }
295 "ascii" => {
296 for line in &buddy.ascii_art {
297 println!(" {line}");
298 }
299 }
300 "json" => match serde_json::to_string_pretty(&buddy) {
301 Ok(json) => println!("{json}"),
302 Err(error) => eprintln!("JSON error: {error}"),
303 },
304 _ => {
305 println!("Usage: nebu-ctx buddy [show|stats|ascii|json]");
306 }
307 }
308}
309
310pub fn cmd_upgrade() {
311 println!("'upgrade' has been renamed to 'update'. Running 'nebu-ctx update' instead.\n");
312 core::updater::run(&[]);
313}