Skip to content

Commit 538efa0

Browse files
authored
feat: add --no-prompt to upgrade (#122)
1 parent ebf79b8 commit 538efa0

3 files changed

Lines changed: 169 additions & 145 deletions

File tree

crates/stellar-scaffold-cli/src/arg_parsing.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ pub enum Error {
2222
ScAddress(#[from] sc_address::Error),
2323
#[error(transparent)]
2424
Config(#[from] config::Error),
25-
// #[error("")]
26-
// HelpMessage(String),
25+
#[error(transparent)]
26+
Dialoguer(#[from] dialoguer::Error),
27+
#[error(transparent)]
28+
Spec(#[from] soroban_spec_tools::contract::Error),
2729
}
2830

2931
pub fn build_custom_cmd(name: &str, spec: &Spec) -> Result<clap::Command, Error> {
@@ -106,3 +108,6 @@ Each arg has a corresponding --<arg_name>-file-path which is a path to a file co
106108
Note: The only types which aren't JSON are Bytes and BytesN, which are raw bytes"
107109
)
108110
}
111+
112+
mod parser;
113+
pub use parser::*;
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
use dialoguer::{Confirm, Input, Select};
2+
3+
use super::Error;
4+
5+
pub struct ArgParser {
6+
skip_prompt: bool,
7+
}
8+
9+
impl ArgParser {
10+
pub fn get_constructor_args(
11+
skip_prompt: bool,
12+
contract_name: &str,
13+
wasm: &[u8],
14+
) -> Result<Option<String>, Error> {
15+
let entries = soroban_spec_tools::contract::Spec::new(wasm)?.spec;
16+
let spec = soroban_spec_tools::Spec::new(entries.clone());
17+
18+
// Check if constructor function exists
19+
let Ok(func) = spec.find_function("__constructor") else {
20+
return Ok(None);
21+
};
22+
if func.inputs.is_empty() {
23+
return Ok(None);
24+
}
25+
26+
// Build the custom command for the constructor
27+
let cmd = super::build_custom_cmd("__constructor", &spec)?;
28+
let parser = Self { skip_prompt };
29+
30+
println!("\n📋 Contract '{contract_name}' requires constructor arguments:");
31+
let args = cmd
32+
.get_arguments()
33+
.filter(|arg| !arg.get_id().as_str().ends_with("-file-path"))
34+
.filter_map(|arg| parser.handle_constructor_argument(arg).transpose())
35+
.collect::<Result<Vec<_>, _>>()?
36+
.join(" ");
37+
Ok((!args.is_empty()).then_some(args))
38+
}
39+
40+
fn handle_constructor_argument(&self, arg: &clap::Arg) -> Result<Option<String>, Error> {
41+
let arg_name = arg.get_id().as_str();
42+
43+
let help_text = arg.get_long_help().or(arg.get_help()).map_or_else(
44+
|| "No description available".to_string(),
45+
ToString::to_string,
46+
);
47+
48+
let value_name = arg
49+
.get_value_names()
50+
.map_or_else(|| "VALUE".to_string(), |names| names.join(" "));
51+
52+
// Display help text before the prompt
53+
println!("\n --{arg_name}");
54+
if value_name != "bool" && !help_text.is_empty() {
55+
println!(" {help_text}");
56+
}
57+
58+
if value_name == "bool" {
59+
self.handle_bool_argument(arg_name)
60+
} else if value_name.contains('|') && is_simple_enum(&value_name) {
61+
self.handle_simple_enum_argument(arg_name, &value_name)
62+
} else {
63+
// For all other types (complex enums, structs, strings), use string input
64+
self.handle_formatted_input(arg_name)
65+
}
66+
}
67+
68+
fn handle_formatted_input(&self, arg_name: &str) -> Result<Option<String>, Error> {
69+
let input_result: String = if self.skip_prompt {
70+
String::new()
71+
} else {
72+
Input::new()
73+
.with_prompt(format!("Enter value for --{arg_name}"))
74+
.allow_empty(true)
75+
.interact()?
76+
};
77+
78+
let value = input_result.trim();
79+
80+
let value = if value.is_empty() {
81+
"# TODO: <Fill in value>"
82+
} else {
83+
// Check if the value is already quoted
84+
let is_already_quoted = (value.starts_with('"') && value.ends_with('"'))
85+
|| (value.starts_with('\'') && value.ends_with('\''));
86+
87+
// Only wrap in quotes if it's not already quoted and contains special characters or spaces
88+
if !is_already_quoted
89+
&& (value.contains(' ')
90+
|| value.contains('{')
91+
|| value.contains('[')
92+
|| value.contains('"'))
93+
{
94+
&format!("'{value}'")
95+
} else {
96+
value
97+
}
98+
};
99+
Ok(Some(format!("--{arg_name} {value}")))
100+
}
101+
102+
fn handle_simple_enum_argument(
103+
&self,
104+
arg_name: &str,
105+
value_name: &str,
106+
) -> Result<Option<String>, Error> {
107+
// Parse the values from "a | b | c" format and add numeric options
108+
let values: Vec<_> = value_name.split('|').collect();
109+
110+
if self.skip_prompt {
111+
return Ok(Some(format!(
112+
"--{arg_name} TODO: Pick One <{}>",
113+
values.join(" | ")
114+
)));
115+
}
116+
117+
let mut select = Select::new()
118+
.with_prompt(format!("Select value for --{arg_name}"))
119+
.default(0); // This will show the cursor on the first option initially
120+
121+
// Add "Skip" option
122+
select = select.item("(Skip - leave blank)");
123+
124+
for value in &values {
125+
select = select.item(format!("Value: {value}"));
126+
}
127+
128+
let selection = select.interact()?;
129+
130+
Ok((selection > 0).then(|| {
131+
// User selected an actual value (not skip)
132+
let selected_value = values[selection - 1];
133+
format!("--{arg_name} {selected_value}")
134+
}))
135+
}
136+
137+
fn handle_bool_argument(&self, arg_name: &str) -> Result<Option<String>, Error> {
138+
if self.skip_prompt {
139+
return Ok(Some(format!("TODO add or remove <--{arg_name}>")));
140+
}
141+
let bool_value = Confirm::new()
142+
.with_prompt(format!("Set --{arg_name} to true?"))
143+
.default(false)
144+
.interact()?;
145+
Ok(bool_value.then(|| format!("--{arg_name}")))
146+
}
147+
}
148+
149+
fn is_simple_enum(value_name: &str) -> bool {
150+
value_name.split('|').all(|part| {
151+
let trimmed = part.trim();
152+
trimmed.parse::<i32>().is_ok() || trimmed.chars().all(|c| c.is_alphabetic() || c == '_')
153+
})
154+
}

crates/stellar-scaffold-cli/src/commands/upgrade.rs

Lines changed: 8 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
use crate::arg_parsing::ArgParser;
12
use crate::commands::build::env_toml::{Account, Contract, Environment, Network};
23
use clap::Parser;
34
use degit::degit;
4-
use dialoguer::{Confirm, Input, Select};
55
use indexmap::IndexMap;
66
use std::fs;
77
use std::fs::{create_dir_all, metadata, read_dir, write};
@@ -20,6 +20,9 @@ pub struct Cmd {
2020
/// The path to the existing workspace (defaults to current directory)
2121
#[arg(default_value = ".")]
2222
pub workspace_path: PathBuf,
23+
/// Skip the prompt to fill in constructor arguments
24+
#[arg(long)]
25+
pub skip_prompt: bool,
2326
}
2427

2528
/// Errors that can occur during upgrade
@@ -47,8 +50,8 @@ pub enum Error {
4750
TomlDeserializeError(#[from] toml::de::Error),
4851
#[error(transparent)]
4952
BuildError(#[from] build::Error),
50-
#[error("Failed to get constructor arguments: {0}")]
51-
ConstructorArgsError(String),
53+
#[error("Failed to get constructor arguments: {0:?}")]
54+
ConstructorArgsError(arg_parsing::Error),
5255
#[error("WASM file not found for contract '{0}'. Please build the contract first.")]
5356
WasmFileNotFound(String),
5457
#[error(transparent)]
@@ -350,146 +353,8 @@ impl Cmd {
350353

351354
// Read the WASM file and get spec entries
352355
let raw_wasm = fs::read(&wasm_path)?;
353-
let entries = soroban_spec_tools::contract::Spec::new(&raw_wasm)?.spec;
354-
let spec = soroban_spec_tools::Spec::new(entries.clone());
355-
356-
// Check if constructor function exists
357-
let Ok(func) = spec.find_function("__constructor") else {
358-
return Ok(None);
359-
};
360-
if func.inputs.is_empty() {
361-
return Ok(None);
362-
}
363-
364-
// Build the custom command for the constructor
365-
let cmd = arg_parsing::build_custom_cmd("__constructor", &spec).map_err(|e| {
366-
Error::ConstructorArgsError(format!("Failed to build constructor command: {e}"))
367-
})?;
368-
369-
println!("\n📋 Contract '{contract_name}' requires constructor arguments:");
370-
371-
let mut args = Vec::new();
372-
373-
// Loop through the command arguments, skipping file args
374-
for arg in cmd.get_arguments() {
375-
let arg_name = arg.get_id().as_str();
376-
377-
// Skip file arguments (they end with -file-path)
378-
if arg_name.ends_with("-file-path") {
379-
continue;
380-
}
381-
382-
if let Some(arg_value) = Self::handle_constructor_argument(arg)? {
383-
args.push(arg_value);
384-
}
385-
}
386-
387-
Ok((!args.is_empty()).then(|| args.join(" ")))
388-
}
389-
390-
fn handle_constructor_argument(arg: &clap::Arg) -> Result<Option<String>, Error> {
391-
let arg_name = arg.get_id().as_str();
392-
393-
let help_text = arg.get_long_help().or(arg.get_help()).map_or_else(
394-
|| "No description available".to_string(),
395-
ToString::to_string,
396-
);
397-
398-
let value_name = arg
399-
.get_value_names()
400-
.map_or_else(|| "VALUE".to_string(), |names| names.join(" "));
401-
402-
// Display help text before the prompt
403-
println!("\n --{arg_name}");
404-
if value_name != "bool" && !help_text.is_empty() {
405-
println!(" {help_text}");
406-
}
407-
408-
if value_name == "bool" {
409-
Self::handle_bool_argument(arg_name)
410-
} else if value_name.contains('|') && Self::is_simple_enum(&value_name) {
411-
Self::handle_simple_enum_argument(arg_name, &value_name)
412-
} else {
413-
// For all other types (complex enums, structs, strings), use string input
414-
Self::handle_formatted_input(arg_name)
415-
}
416-
}
417-
418-
fn is_simple_enum(value_name: &str) -> bool {
419-
value_name.split('|').all(|part| {
420-
let trimmed = part.trim();
421-
trimmed.parse::<i32>().is_ok() || trimmed.chars().all(|c| c.is_alphabetic() || c == '_')
422-
})
423-
}
424-
425-
fn handle_formatted_input(arg_name: &str) -> Result<Option<String>, Error> {
426-
let input_result: Result<String, _> = Input::new()
427-
.with_prompt(format!("Enter value for --{arg_name}"))
428-
.allow_empty(true)
429-
.interact();
430-
431-
let value = input_result
432-
.as_deref()
433-
.map(str::trim)
434-
.map_err(|e| Error::ConstructorArgsError(format!("Input error: {e}")))?;
435-
436-
let value = if value.is_empty() {
437-
"# TODO: Fill in value"
438-
} else {
439-
// Check if the value is already quoted
440-
let is_already_quoted = (value.starts_with('"') && value.ends_with('"'))
441-
|| (value.starts_with('\'') && value.ends_with('\''));
442-
443-
// Only wrap in quotes if it's not already quoted and contains special characters or spaces
444-
if !is_already_quoted
445-
&& (value.contains(' ')
446-
|| value.contains('{')
447-
|| value.contains('[')
448-
|| value.contains('"'))
449-
{
450-
&format!("'{value}'")
451-
} else {
452-
value
453-
}
454-
};
455-
Ok(Some(format!("--{arg_name} {value}")))
456-
}
457-
458-
fn handle_simple_enum_argument(
459-
arg_name: &str,
460-
value_name: &str,
461-
) -> Result<Option<String>, Error> {
462-
let mut select = Select::new()
463-
.with_prompt(format!("Select value for --{arg_name}"))
464-
.default(0); // This will show the cursor on the first option initially
465-
466-
// Add "Skip" option
467-
select = select.item("(Skip - leave blank)");
468-
469-
// Parse the values from "a | b | c" format and add numeric options
470-
let values: Vec<_> = value_name.split('|').collect();
471-
for value in &values {
472-
select = select.item(format!("Value: {value}"));
473-
}
474-
475-
let selection = select
476-
.interact()
477-
.map_err(|e| Error::ConstructorArgsError(format!("Input error: {e}")))?;
478-
479-
Ok((selection > 0).then(|| {
480-
// User selected an actual value (not skip)
481-
let selected_value = values[selection - 1];
482-
format!("--{arg_name} {selected_value}")
483-
}))
484-
}
485-
486-
fn handle_bool_argument(arg_name: &str) -> Result<Option<String>, Error> {
487-
let bool_value = Confirm::new()
488-
.with_prompt(format!("Set --{arg_name} to true?"))
489-
.default(false)
490-
.interact()
491-
.map_err(|e| Error::ConstructorArgsError(format!("Input error: {e}")))?;
492-
Ok(bool_value.then(|| format!("--{arg_name}")))
356+
ArgParser::get_constructor_args(self.skip_prompt, contract_name, &raw_wasm)
357+
.map_err(Error::ConstructorArgsError)
493358
}
494359

495360
fn setup_env_file(&self) -> Result<(), Error> {

0 commit comments

Comments
 (0)