1use crate::scaffold::{scaffold_bot, ScaffoldOptions};
4use crate::validate::{validate_bot, Severity, ValidateOptions, DEFAULT_MAX_UPLOAD_BYTES};
5use anyhow::{bail, Context, Result};
6use chipzen_bot::{connect_to_chipzen, load_chipzen_config, resolve_token, ChipzenConfig, EnvName};
7use clap::{Parser, Subcommand};
8use std::path::PathBuf;
9
10const ABOUT: &str = "Chipzen poker bot SDK — scaffold and validate bot projects";
11
12#[derive(Debug, Parser)]
13#[command(name = "chipzen-sdk", version, about = ABOUT, long_about = None)]
14pub struct Cli {
15 #[command(subcommand)]
16 pub command: Command,
17}
18
19#[derive(Debug, Subcommand)]
20pub enum Command {
21 Init {
23 name: String,
25 #[arg(long)]
27 dir: Option<PathBuf>,
28 },
29 Validate {
50 path: PathBuf,
52 #[arg(long)]
54 max_size_mb: Option<u64>,
55 #[arg(long)]
57 no_color: bool,
58 },
59 RunExternal {
71 #[arg(long, value_parser = ["prod", "staging", "local"])]
74 env: Option<String>,
75 #[arg(long)]
78 token: Option<String>,
79 #[arg(long)]
82 bot_id: Option<String>,
83 },
84}
85
86pub fn run(cli: Cli) -> Result<()> {
87 match cli.command {
88 Command::Init { name, dir } => run_init(&name, dir),
89 Command::Validate {
90 path,
91 max_size_mb,
92 no_color,
93 } => run_validate(&path, max_size_mb, no_color),
94 Command::RunExternal { env, token, bot_id } => {
95 run_external(env.as_deref(), token.as_deref(), bot_id.as_deref())
96 }
97 }
98}
99
100fn run_external(
104 env: Option<&str>,
105 explicit_token: Option<&str>,
106 explicit_bot_id: Option<&str>,
107) -> Result<()> {
108 let config: Option<ChipzenConfig> =
109 load_chipzen_config(None).context("reading chipzen.toml")?;
110
111 let config_url = config.as_ref().and_then(|c| c.url.clone());
113 let (url, resolved_env) = if let Some(url) = config_url {
114 (url, None)
115 } else {
116 let bot_id = explicit_bot_id
118 .map(str::to_string)
119 .or_else(|| config.as_ref().and_then(|c| c.bot_id.clone()));
120 let Some(bot_id) = bot_id.filter(|s| !s.is_empty()) else {
121 bail!(
122 "No lobby URL is configured. Either:\n \
123 - Pass --bot-id <id>, or\n \
124 - Set [external_api].bot_id in chipzen.toml, or\n \
125 - Set [external_api].url in chipzen.toml for a verbatim URL."
126 );
127 };
128 let env_name = env
129 .map(|e| EnvName::parse(e).ok_or_else(|| anyhow::anyhow!("unknown env {e:?}")))
130 .transpose()?;
131 let conn = connect_to_chipzen(&bot_id, env_name, None, config.clone())
132 .map_err(|e| anyhow::anyhow!("{e}"))?;
133 (conn.url, conn.env.map(|e| e.as_str().to_string()))
134 };
135
136 let token = resolve_token(explicit_token, config.as_ref());
137
138 println!("Chipzen external-API connection");
139 println!("{}", "=".repeat(50));
140 println!(" lobby URL : {url}");
141 if let Some(env) = resolved_env {
142 println!(" env : {env}");
143 }
144 println!(
145 " token : {}",
146 if token.is_some() {
147 "present (cz_extbot_…)"
148 } else {
149 "MISSING — pass --token or set [external_api].token in chipzen.toml"
150 }
151 );
152 if let Some(cfg) = config.as_ref().and_then(|c| c.path.as_ref()) {
153 println!(" config : {}", cfg.display());
154 } else {
155 println!(" config : none found on the search path");
156 }
157 println!();
158 if token.is_none() {
159 bail!("no external-API token resolved — cannot run a remote-play session");
160 }
161 println!(
162 "Setup looks good. Wire chipzen_bot::run_external_cli(|| MyBot, args) into your bot\n\
163 binary's main to play (the scaffolded starter's `run-external` mode does this)."
164 );
165 Ok(())
166}
167
168fn run_init(name: &str, parent_dir: Option<PathBuf>) -> Result<()> {
169 let opts = ScaffoldOptions { parent_dir };
170 let created =
171 scaffold_bot(name, &opts).with_context(|| format!("scaffolding project {name:?}"))?;
172 println!("Created bot project: {}", created.display());
173 println!();
174 println!("Next steps:");
175 println!(" cd {name}");
176 println!(" cargo build");
177 println!(" # Edit src/main.rs to implement your strategy");
178 println!(" chipzen-sdk validate .");
179 Ok(())
180}
181
182fn run_validate(path: &std::path::Path, max_size_mb: Option<u64>, no_color: bool) -> Result<()> {
183 let opts = ValidateOptions {
184 max_upload_bytes: max_size_mb
185 .map(|mb| mb * 1024 * 1024)
186 .unwrap_or(DEFAULT_MAX_UPLOAD_BYTES),
187 };
188 let results = validate_bot(path, &opts)?;
189 print_results(&results, !no_color);
190
191 let fails = results
192 .iter()
193 .filter(|r| matches!(r.severity, Severity::Fail))
194 .count();
195 if fails > 0 {
196 std::process::exit(1);
197 }
198 Ok(())
199}
200
201fn print_results(results: &[crate::validate::ValidationResult], color: bool) {
202 let supports_color = color && std::io::IsTerminal::is_terminal(&std::io::stdout());
203 let green = if supports_color { "\x1b[92m" } else { "" };
204 let yellow = if supports_color { "\x1b[93m" } else { "" };
205 let red = if supports_color { "\x1b[91m" } else { "" };
206 let reset = if supports_color { "\x1b[0m" } else { "" };
207
208 println!();
209 println!("Chipzen Bot Validation");
210 println!("{}", "=".repeat(50));
211 for r in results {
212 let icon = match r.severity {
213 Severity::Pass => format!("{green}PASS{reset}"),
214 Severity::Warn => format!("{yellow}WARN{reset}"),
215 Severity::Fail => format!("{red}FAIL{reset}"),
216 };
217 println!(" [{icon}] {}: {}", r.name, r.message);
218 }
219
220 println!();
221 let fails = results
222 .iter()
223 .filter(|r| matches!(r.severity, Severity::Fail))
224 .count();
225 if fails > 0 {
226 let plural = if fails == 1 { "" } else { "s" };
227 println!("{red}{fails} check{plural} failed.{reset}");
228 } else {
229 println!("{green}All checks passed! Your bot is ready to upload.{reset}");
230 }
231}