cargo_airbender/commands/new/
args.rs1use crate::cli::{NewAllocatorArg, NewArgs, NewProverBackendArg};
2use crate::error::{CliError, Result};
3use dialoguer::{Confirm, Input, Select};
4use std::path::{Component, Path, PathBuf};
5
6pub(super) struct ResolvedNewArgs {
7 pub(super) path: PathBuf,
8 pub(super) project_name: String,
9 pub(super) enable_std: bool,
10 pub(super) allocator: NewAllocatorArg,
11 pub(super) prover_backend: NewProverBackendArg,
12 pub(super) sdk_path: Option<PathBuf>,
13 pub(super) sdk_version: Option<String>,
14}
15
16pub(super) fn resolve_new_args(args: NewArgs) -> Result<ResolvedNewArgs> {
17 let NewArgs {
18 path,
19 name,
20 enable_std,
21 allocator,
22 prover_backend,
23 yes,
24 sdk_path,
25 sdk_version,
26 } = args;
27
28 let path = path.unwrap_or_else(|| PathBuf::from("."));
29 ensure_empty_dir(&path)?;
30
31 let inferred_name = infer_project_name(&path)?;
32 let default_name = name.or(inferred_name);
33
34 let (project_name, enable_std, allocator, prover_backend) = if yes {
35 let project_name = default_name.ok_or_else(|| {
36 CliError::new(format!(
37 "could not infer project name from destination `{}`",
38 path.display()
39 ))
40 .with_hint("pass an explicit project name with `--name <project-name>`")
41 })?;
42
43 (project_name, enable_std, allocator, prover_backend)
44 } else {
45 ensure_interactive_terminal()?;
46 (
47 prompt_project_name(default_name.as_deref())?,
48 prompt_enable_std(enable_std)?,
49 prompt_allocator(allocator)?,
50 prompt_prover_backend(prover_backend)?,
51 )
52 };
53
54 Ok(ResolvedNewArgs {
55 path,
56 project_name,
57 enable_std,
58 allocator,
59 prover_backend,
60 sdk_path,
61 sdk_version,
62 })
63}
64
65fn infer_project_name(path: &Path) -> Result<Option<String>> {
66 if let Some(name) = normalize_name(path.file_name()) {
67 return Ok(Some(name));
68 }
69
70 if is_current_dir_path(path) {
71 let current_dir = std::env::current_dir()
72 .map_err(|err| CliError::with_source("failed to determine current directory", err))?;
73 return Ok(normalize_name(current_dir.file_name()));
74 }
75
76 Ok(None)
77}
78
79fn is_current_dir_path(path: &Path) -> bool {
80 let mut components = path.components();
81 matches!(components.next(), Some(Component::CurDir))
82 && components.all(|component| component == Component::CurDir)
83}
84
85fn normalize_name(component: Option<&std::ffi::OsStr>) -> Option<String> {
86 component
87 .map(|value| value.to_string_lossy().trim().to_string())
88 .filter(|value| !value.is_empty() && value != "." && value != "..")
89}
90
91fn ensure_interactive_terminal() -> Result<()> {
92 use std::io::IsTerminal;
93
94 if std::io::stdin().is_terminal() && std::io::stdout().is_terminal() {
95 return Ok(());
96 }
97
98 Err(CliError::new("interactive mode requires a terminal")
99 .with_hint("pass `--yes` to run non-interactively and provide values via flags"))
100}
101
102fn prompt_project_name(default_name: Option<&str>) -> Result<String> {
103 let mut prompt = Input::<String>::new().with_prompt("Name of the project");
104 if let Some(default_name) = default_name {
105 prompt = prompt.with_initial_text(default_name.to_string());
106 }
107
108 let project_name = prompt
109 .validate_with(|input: &String| {
110 if input.trim().is_empty() {
111 Err("project name cannot be empty")
112 } else {
113 Ok(())
114 }
115 })
116 .interact_text()
117 .map_err(map_prompt_error)?;
118
119 Ok(project_name.trim().to_string())
120}
121
122fn prompt_enable_std(default: bool) -> Result<bool> {
123 Confirm::new()
124 .with_prompt("Enable STD")
125 .default(default)
126 .interact()
127 .map_err(map_prompt_error)
128}
129
130fn prompt_allocator(default: NewAllocatorArg) -> Result<NewAllocatorArg> {
131 let options = ["talc", "bump", "custom"];
132 let default_index = match default {
133 NewAllocatorArg::Talc => 0,
134 NewAllocatorArg::Bump => 1,
135 NewAllocatorArg::Custom => 2,
136 };
137
138 let selected = Select::new()
139 .with_prompt("Which allocator to use")
140 .items(&options)
141 .default(default_index)
142 .interact()
143 .map_err(map_prompt_error)?;
144
145 Ok(match selected {
146 0 => NewAllocatorArg::Talc,
147 1 => NewAllocatorArg::Bump,
148 2 => NewAllocatorArg::Custom,
149 _ => {
150 return Err(CliError::new(format!(
151 "invalid allocator selection index `{selected}`"
152 )));
153 }
154 })
155}
156
157fn prompt_prover_backend(default: NewProverBackendArg) -> Result<NewProverBackendArg> {
158 let options = [
159 "dev (mock proof for development)",
160 "gpu (real proving; CUDA required)",
161 ];
162 let default_index = match default {
163 NewProverBackendArg::Dev => 0,
164 NewProverBackendArg::Gpu => 1,
165 };
166
167 let selected = Select::new()
168 .with_prompt("Which prover backend to use")
169 .items(&options)
170 .default(default_index)
171 .interact()
172 .map_err(map_prompt_error)?;
173
174 Ok(match selected {
175 0 => NewProverBackendArg::Dev,
176 1 => NewProverBackendArg::Gpu,
177 _ => {
178 return Err(CliError::new(format!(
179 "invalid prover backend selection index `{selected}`"
180 )));
181 }
182 })
183}
184
185fn map_prompt_error(err: dialoguer::Error) -> CliError {
186 CliError::with_source("failed to read interactive input", err)
187 .with_hint("pass `--yes` to run non-interactively and provide values via flags")
188}
189
190fn ensure_empty_dir(path: &Path) -> Result<()> {
191 if path.exists()
192 && path
193 .read_dir()
194 .map_err(|err| {
195 CliError::with_source(
196 format!("failed to list directory `{}`", path.display()),
197 err,
198 )
199 })?
200 .next()
201 .is_some()
202 {
203 return Err(CliError::new(format!(
204 "destination directory `{}` is not empty",
205 path.display()
206 ))
207 .with_hint("choose a new path or remove existing files in that directory"));
208 }
209
210 Ok(())
211}