@devaloop/devalang 0.0.1-alpha.10 → 0.0.1-alpha.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.devalang +6 -1
  2. package/Cargo.toml +6 -2
  3. package/README.md +59 -142
  4. package/docs/CHANGELOG.md +60 -1
  5. package/docs/ROADMAP.md +1 -1
  6. package/docs/TODO.md +1 -1
  7. package/examples/bank.deva +9 -0
  8. package/examples/duration.deva +9 -0
  9. package/examples/index.deva +6 -6
  10. package/out-tsc/bin/devalang.exe +0 -0
  11. package/package.json +2 -1
  12. package/project-version.json +3 -3
  13. package/rust/cli/bank.rs +455 -0
  14. package/rust/cli/build.rs +1 -1
  15. package/rust/cli/check.rs +1 -1
  16. package/rust/cli/driver.rs +280 -0
  17. package/rust/cli/install.rs +17 -0
  18. package/rust/cli/mod.rs +5 -200
  19. package/rust/cli/play.rs +1 -1
  20. package/rust/cli/update.rs +4 -0
  21. package/rust/common/cdn.rs +11 -0
  22. package/rust/common/mod.rs +1 -0
  23. package/rust/config/driver.rs +76 -0
  24. package/rust/config/loader.rs +98 -1
  25. package/rust/config/mod.rs +1 -15
  26. package/rust/core/audio/engine.rs +151 -10
  27. package/rust/core/audio/interpreter/arrow_call.rs +17 -4
  28. package/rust/core/audio/interpreter/trigger.rs +56 -2
  29. package/rust/core/audio/loader/trigger.rs +12 -0
  30. package/rust/core/lexer/handler/driver.rs +12 -1
  31. package/rust/core/lexer/handler/mod.rs +1 -0
  32. package/rust/core/lexer/handler/slash.rs +21 -0
  33. package/rust/core/lexer/token.rs +1 -0
  34. package/rust/core/parser/driver.rs +36 -2
  35. package/rust/core/parser/handler/arrow_call.rs +29 -4
  36. package/rust/core/parser/handler/dot.rs +102 -37
  37. package/rust/core/preprocessor/loader.rs +93 -14
  38. package/rust/core/preprocessor/resolver/driver.rs +5 -0
  39. package/rust/core/shared/bank.rs +21 -0
  40. package/rust/core/shared/duration.rs +1 -0
  41. package/rust/core/shared/mod.rs +2 -1
  42. package/rust/core/shared/value.rs +1 -0
  43. package/rust/installer/bank.rs +55 -0
  44. package/rust/installer/mod.rs +2 -0
  45. package/rust/installer/utils.rs +56 -0
  46. package/rust/main.rs +62 -5
  47. package/docs/COMMANDS.md +0 -85
  48. package/docs/CONFIG.md +0 -30
  49. package/docs/SYNTAX.md +0 -230
@@ -6,14 +6,54 @@ use crate::core::{
6
6
  };
7
7
 
8
8
  pub fn parse_dot_token(parser: &mut Parser, _global_store: &mut GlobalStore) -> Statement {
9
- parser.advance(); // consume the dot token
9
+ parser.advance(); // consume the first dot
10
10
 
11
11
  let Some(dot_token) = parser.previous_clone() else {
12
12
  return Statement::unknown();
13
13
  };
14
14
 
15
- // .kick
16
- let Some(entity_token) = parser.peek_clone() else {
15
+ // Parse namespaced identifier: .808.kick.snare
16
+ let mut parts = Vec::new();
17
+
18
+ while let Some(token) = parser.peek_clone() {
19
+ match token.kind {
20
+ TokenKind::Number => {
21
+ // Stop if it's part of a duration
22
+ if let Some(TokenKind::Slash) = parser.peek_nth_kind(1) {
23
+ break;
24
+ }
25
+
26
+ parts.push(token.lexeme.clone());
27
+ parser.advance();
28
+ }
29
+
30
+ TokenKind::Identifier => {
31
+ // Stop parsing entity name if next token is ':' or if already have one ident and current might be a param
32
+ if parts.len() >= 1 {
33
+ break; // we've already got the entity
34
+ }
35
+
36
+ if token.lexeme == "auto" {
37
+ break;
38
+ }
39
+
40
+ parts.push(token.lexeme.clone());
41
+ parser.advance();
42
+ }
43
+
44
+ TokenKind::Dot => {
45
+ parser.advance(); // continue chaining
46
+ }
47
+
48
+ _ => {
49
+ break;
50
+ }
51
+ }
52
+ }
53
+
54
+ let entity = if parts.len() == 1 { parts[0].clone() } else { parts[..=1].join(".") };
55
+
56
+ if entity.is_empty() {
17
57
  return Statement {
18
58
  kind: StatementKind::Trigger {
19
59
  entity: String::new(),
@@ -24,68 +64,93 @@ pub fn parse_dot_token(parser: &mut Parser, _global_store: &mut GlobalStore) ->
24
64
  line: dot_token.line,
25
65
  column: dot_token.column,
26
66
  };
27
- };
28
-
29
- parser.advance(); // consume entity
30
- let entity = entity_token.lexeme.clone();
67
+ }
31
68
 
32
69
  // Check if there's a duration
33
70
  let next = parser.peek_clone();
34
71
 
35
72
  let (duration, value) = match next {
36
- // If no more tokens, it's just `.kick`
37
73
  None => (Duration::Auto, Value::Null),
38
74
 
39
75
  Some(token) =>
40
76
  match token.kind {
41
- TokenKind::Newline | TokenKind::EOF => { (Duration::Auto, Value::Null) }
77
+ TokenKind::Newline | TokenKind::EOF => (Duration::Auto, Value::Null),
42
78
 
43
79
  TokenKind::Number => {
44
- let duration_lexeme = token.lexeme.clone();
45
- parser.advance(); // consume duration
80
+ let numerator = token.lexeme.clone();
81
+ parser.advance(); // consume numerator
82
+
83
+ if let Some(TokenKind::Slash) = parser.peek_kind() {
84
+ parser.advance(); // consume slash
85
+
86
+ if let Some(denominator_token) = parser.peek_clone() {
87
+ if denominator_token.kind == TokenKind::Number {
88
+ let denominator = denominator_token.lexeme.clone();
89
+ parser.advance(); // consume denominator
90
+
91
+ let beat_str = format!("{}/{}", numerator, denominator);
92
+ let beat_duration = Duration::Beat(beat_str);
93
+
94
+ let val = match parser.peek_clone() {
95
+ Some(param_token) if
96
+ param_token.kind == TokenKind::Identifier
97
+ => {
98
+ parser.advance();
99
+ Value::Identifier(param_token.lexeme.clone())
100
+ }
101
+ Some(param_token) if param_token.kind == TokenKind::LBrace => {
102
+ parser.parse_map_value().unwrap_or(Value::Null)
103
+ }
104
+ _ => Value::Null,
105
+ };
106
+
107
+ return Statement {
108
+ kind: StatementKind::Trigger {
109
+ entity,
110
+ duration: beat_duration,
111
+ },
112
+ value: val,
113
+ indent: dot_token.indent,
114
+ line: dot_token.line,
115
+ column: dot_token.column,
116
+ };
117
+ }
118
+ }
119
+ }
46
120
 
47
- // Try to parse optional value (ex: .kick 250 params)
48
- match parser.peek_clone() {
121
+ // fallback: simple numeric duration
122
+ let duration = parse_duration(numerator);
123
+
124
+ let val = match parser.peek_clone() {
49
125
  Some(param_token) if param_token.kind == TokenKind::Identifier => {
50
126
  parser.advance();
51
- (
52
- parse_duration(duration_lexeme),
53
- Value::Identifier(param_token.lexeme.clone()),
54
- )
127
+ Value::Identifier(param_token.lexeme.clone())
55
128
  }
56
-
57
129
  Some(param_token) if param_token.kind == TokenKind::LBrace => {
58
- // Handle value as Map
59
- let map = parser.parse_map_value(); // Assumes you have a helper for map
60
- (parse_duration(duration_lexeme), map.unwrap_or(Value::Null))
130
+ parser.parse_map_value().unwrap_or(Value::Null)
61
131
  }
132
+ _ => Value::Null,
133
+ };
62
134
 
63
- _ => (parse_duration(duration_lexeme), Value::Null),
64
- }
135
+ (duration, val)
65
136
  }
66
137
 
67
138
  TokenKind::Identifier => {
68
139
  let duration_lexeme = token.lexeme.clone();
69
140
  parser.advance(); // consume duration
70
141
 
71
- // Try to parse optional value (ex: .kick auto params)
72
- match parser.peek_clone() {
142
+ let val = match parser.peek_clone() {
73
143
  Some(param_token) if param_token.kind == TokenKind::Identifier => {
74
144
  parser.advance();
75
- (
76
- parse_duration(duration_lexeme),
77
- Value::Identifier(param_token.lexeme.clone()),
78
- )
145
+ Value::Identifier(param_token.lexeme.clone())
79
146
  }
80
-
81
147
  Some(param_token) if param_token.kind == TokenKind::LBrace => {
82
- // Handle value as Map
83
- let map = parser.parse_map_value(); // Assumes you have a helper for map
84
- (parse_duration(duration_lexeme), map.unwrap_or(Value::Null))
148
+ parser.parse_map_value().unwrap_or(Value::Null)
85
149
  }
150
+ _ => Value::Null,
151
+ };
86
152
 
87
- _ => (parse_duration(duration_lexeme), Value::Null),
88
- }
153
+ (parse_duration(duration_lexeme), val)
89
154
  }
90
155
 
91
156
  _ => (Duration::Auto, Value::Null),
@@ -104,8 +169,8 @@ pub fn parse_dot_token(parser: &mut Parser, _global_store: &mut GlobalStore) ->
104
169
  fn parse_duration(s: String) -> Duration {
105
170
  if s == "auto" {
106
171
  Duration::Auto
107
- } else if s.parse::<f32>().is_ok() {
108
- Duration::Number(s.parse().unwrap())
172
+ } else if let Ok(num) = s.parse::<f32>() {
173
+ Duration::Number(num)
109
174
  } else {
110
175
  Duration::Identifier(s)
111
176
  }
@@ -1,10 +1,11 @@
1
- use std::{ collections::HashMap, path::Path };
1
+ use std::{ collections::{ HashMap, HashSet }, path::Path };
2
2
  use crate::{
3
3
  core::{
4
4
  error::ErrorHandler,
5
5
  lexer::{ token::Token, Lexer },
6
- parser::{ statement::{ Statement, StatementKind }, driver::Parser },
6
+ parser::{ driver::Parser, statement::{ Statement, StatementKind } },
7
7
  preprocessor::{ module::Module, processor::process_modules },
8
+ shared::{ bank::BankFile, value::Value },
8
9
  store::global::GlobalStore,
9
10
  utils::path::normalize_path,
10
11
  },
@@ -86,15 +87,20 @@ impl ModuleLoader {
86
87
  let statements = parser.parse_tokens(tokens, global_store);
87
88
  module.statements = statements;
88
89
 
90
+ // SECTION Injecting bank triggers if any
91
+ if let Err(e) = self.inject_bank_triggers(&mut module, "808") {
92
+ return Err(format!("Failed to inject bank triggers: {}", e));
93
+ }
94
+
95
+ global_store.modules.insert(self.entry.clone(), module.clone());
96
+
89
97
  // SECTION Error handling
90
98
  let mut error_handler = ErrorHandler::new();
91
99
  error_handler.detect_from_statements(&mut parser, &module.statements);
92
100
 
93
- global_store.modules.insert(self.entry.clone(), module.clone());
94
-
95
101
  Ok(module)
96
102
  }
97
-
103
+
98
104
  pub fn load_wasm_module(&self, global_store: &mut GlobalStore) -> Result<(), String> {
99
105
  // Step one : Load the module from the global store
100
106
  let module = {
@@ -170,6 +176,23 @@ impl ModuleLoader {
170
176
 
171
177
  let statements = parser.parse_tokens(tokens.clone(), global_store);
172
178
 
179
+ // Insert module into store
180
+ let mut module = Module::new(&path);
181
+ module.tokens = tokens.clone();
182
+ module.statements = statements.clone();
183
+
184
+ // Inject triggers for each bank used in module
185
+ for bank_name in ModuleLoader::extract_bank_names(&statements) {
186
+ if let Err(e) = self.inject_bank_triggers(&mut module, &bank_name) {
187
+ return HashMap::new(); // Return empty map on error
188
+ }
189
+ }
190
+
191
+ global_store.insert_module(path.clone(), module);
192
+
193
+ // Load dependencies
194
+ self.load_module_imports(&path, global_store);
195
+
173
196
  // Error handling
174
197
  let mut error_handler = ErrorHandler::new();
175
198
  error_handler.detect_from_statements(&mut parser, &statements);
@@ -182,15 +205,6 @@ impl ModuleLoader {
182
205
  }
183
206
  }
184
207
 
185
- // Insert module into store
186
- let mut module = Module::new(&path);
187
- module.tokens = tokens.clone();
188
- module.statements = statements.clone();
189
- global_store.insert_module(path.clone(), module);
190
-
191
- // Load dependencies
192
- self.load_module_imports(&path, global_store);
193
-
194
208
  // Return tokens per module
195
209
  global_store.modules
196
210
  .iter()
@@ -226,4 +240,69 @@ impl ModuleLoader {
226
240
  self.load_module_recursively(&resolved, global_store);
227
241
  }
228
242
  }
243
+
244
+ pub fn inject_bank_triggers(&self, module: &mut Module, bank_name: &str) -> Result<(), String> {
245
+ let bank_path = Path::new("./.deva/bank").join(bank_name);
246
+ let bank_file_path = bank_path.join("bank.toml");
247
+
248
+ if !bank_file_path.exists() {
249
+ return Ok(()); // Pas d'erreur si la banque n'existe pas encore
250
+ }
251
+
252
+ let content = std::fs
253
+ ::read_to_string(&bank_file_path)
254
+ .map_err(|e| format!("Failed to read '{}': {}", bank_file_path.display(), e))?;
255
+
256
+ let parsed: BankFile = toml
257
+ ::from_str(&content)
258
+ .map_err(|e| format!("Failed to parse '{}': {}", bank_file_path.display(), e))?;
259
+
260
+ let mut bank_map = HashMap::new();
261
+
262
+ for bank_trigger in parsed.triggers.unwrap_or_default() {
263
+ let trigger_name = bank_trigger.name.clone().replace("./", "");
264
+ let bank_trigger_path = format!("devalang://bank/{}/{}", bank_name, trigger_name);
265
+
266
+ bank_map.insert(bank_trigger.name.clone(), Value::String(bank_trigger_path.clone()));
267
+
268
+ if module.variable_table.variables.contains_key(bank_name) {
269
+ eprintln!(
270
+ "⚠️ Trigger '{}' already defined in module '{}', skipping injection.",
271
+ bank_name, module.path
272
+ );
273
+ continue;
274
+ }
275
+
276
+ module.variable_table.set(
277
+ format!("{}.{}", bank_name, bank_trigger.name),
278
+ Value::String(bank_trigger_path.clone())
279
+ );
280
+ }
281
+
282
+ // Inject the map under the bank name
283
+ module.variable_table.set(bank_name.to_string(), Value::Map(bank_map));
284
+
285
+ Ok(())
286
+ }
287
+
288
+ fn extract_bank_names(statements: &[Statement]) -> HashSet<String> {
289
+ let mut banks = HashSet::new();
290
+
291
+ for stmt in statements {
292
+ if let StatementKind::Trigger { entity, .. } = &stmt.kind {
293
+ let parts: Vec<&str> = entity.split('.').collect();
294
+ if parts.len() >= 2 {
295
+ banks.insert(parts[0].to_string()); // "808.kick" → "808"
296
+ }
297
+ }
298
+
299
+ if let StatementKind::Bank = &stmt.kind {
300
+ if let Value::String(name) = &stmt.value {
301
+ banks.insert(name.clone());
302
+ }
303
+ }
304
+ }
305
+
306
+ banks
307
+ }
229
308
  }
@@ -73,6 +73,11 @@ fn resolve_value(value: &Value, module: &Module, global_store: &mut GlobalStore)
73
73
  Value::Null
74
74
  }
75
75
 
76
+ Value::Beat(beat_str) => {
77
+ println!("[warn] '{:?}': unresolved beat '{}'", module.path, beat_str);
78
+ Value::Beat(beat_str.clone())
79
+ }
80
+
76
81
  Value::Map(map) => {
77
82
  let mut resolved = HashMap::new();
78
83
  for (k, v) in map {
@@ -0,0 +1,21 @@
1
+ use serde::{ Deserialize, Serialize };
2
+
3
+ #[derive(Debug, Deserialize)]
4
+ pub struct BankInfo {
5
+ pub name: String,
6
+ pub version: String,
7
+ pub description: String,
8
+ pub author: String,
9
+ }
10
+
11
+ #[derive(Debug, Deserialize)]
12
+ pub struct BankFile {
13
+ pub bank: BankInfo,
14
+ pub triggers: Option<Vec<BankTrigger>>,
15
+ }
16
+
17
+ #[derive(Debug, Deserialize)]
18
+ pub struct BankTrigger {
19
+ pub name: String,
20
+ pub path: String,
21
+ }
@@ -4,5 +4,6 @@ use serde::{ Deserialize, Serialize };
4
4
  pub enum Duration {
5
5
  Number(f32),
6
6
  Identifier(String),
7
+ Beat(String),
7
8
  Auto,
8
9
  }
@@ -1,2 +1,3 @@
1
1
  pub mod value;
2
- pub mod duration;
2
+ pub mod duration;
3
+ pub mod bank;
@@ -13,6 +13,7 @@ pub enum Value {
13
13
  Map(HashMap<String, Value>),
14
14
  Block(Vec<Statement>),
15
15
  Sample(String),
16
+ Beat(String),
16
17
  Unknown,
17
18
  Null,
18
19
  }
@@ -0,0 +1,55 @@
1
+ use std::path::{ Path, PathBuf };
2
+ use crate::{
3
+ common::cdn::get_cdn_url,
4
+ config::loader::{ add_bank_to_config, load_config },
5
+ installer::utils::{ download_file, extract_archive },
6
+ };
7
+
8
+ pub async fn install_bank(name: &str, target_dir: &Path) -> Result<(), String> {
9
+ let cdn_url = get_cdn_url();
10
+ let url = format!("{}/bank/{}", cdn_url, name);
11
+
12
+ let bank_dir = target_dir.join("bank");
13
+ let archive_path = PathBuf::from(format!("./.deva/tmp/{}.devabank", name));
14
+ let extract_path = bank_dir.join(name);
15
+
16
+ if extract_path.exists() {
17
+ println!(
18
+ "Bank '{}' already exists at '{}'. Skipping install.",
19
+ name,
20
+ extract_path.display()
21
+ );
22
+ return Ok(());
23
+ }
24
+
25
+ download_file(&url, &archive_path).await.map_err(|e| format!("Failed to download: {}", e))?;
26
+
27
+ extract_archive(&archive_path, &extract_path).await.map_err(|e|
28
+ format!("Failed to extract: {}", e)
29
+ )?;
30
+
31
+ // Add the bank to the config
32
+ let root_dir = target_dir
33
+ .parent()
34
+ .ok_or_else(|| "Failed to determine root directory".to_string())?;
35
+
36
+ let config_path = root_dir.join(".devalang");
37
+ if !config_path.exists() {
38
+ return Err(
39
+ format!(
40
+ "Config file not found at '{}'. Please run 'devalang init' before adding an addon",
41
+ config_path.display()
42
+ )
43
+ );
44
+ }
45
+
46
+ let mut config = load_config(Some(&config_path)).ok_or_else(||
47
+ format!("Failed to load config from '{}'", config_path.display())
48
+ )?;
49
+
50
+ let dependency_path = &format!("devalang://bank/{}", name);
51
+
52
+ add_bank_to_config(&mut config, &extract_path, &dependency_path);
53
+
54
+ Ok(())
55
+ }
@@ -0,0 +1,2 @@
1
+ pub mod bank;
2
+ pub mod utils;
@@ -0,0 +1,56 @@
1
+ use std::fs::File;
2
+ use std::path::Path;
3
+ use std::io::BufReader;
4
+ use std::error::Error;
5
+ use std::io::{ copy, Cursor };
6
+ use zip::ZipArchive;
7
+
8
+ pub async fn download_file(url: &str, destination: &Path) -> Result<(), Box<dyn Error>> {
9
+ let response = reqwest::get(url).await?;
10
+
11
+ if !response.status().is_success() {
12
+ return Err(format!("Failed to download file: HTTP {}", response.status()).into());
13
+ }
14
+
15
+ if let Some(parent) = destination.parent() {
16
+ std::fs::create_dir_all(parent)?;
17
+ }
18
+
19
+ let bytes = response.bytes().await?;
20
+ let mut content = Cursor::new(bytes);
21
+ let mut file = File::create(destination)?;
22
+ copy(&mut content, &mut file)?;
23
+
24
+ Ok(())
25
+ }
26
+
27
+ pub async fn extract_archive(
28
+ zip_path: &Path,
29
+ destination: &Path
30
+ ) -> Result<(), Box<dyn std::error::Error>> {
31
+ let file = File::open(zip_path)?;
32
+ let mut archive = ZipArchive::new(BufReader::new(file))?;
33
+
34
+ for i in 0..archive.len() {
35
+ let mut file = archive.by_index(i)?;
36
+ let outpath = destination.join(file.mangled_name());
37
+
38
+ if file.name().ends_with('/') {
39
+ std::fs::create_dir_all(&outpath)?;
40
+ } else {
41
+ if let Some(p) = outpath.parent() {
42
+ std::fs::create_dir_all(p)?;
43
+ }
44
+
45
+ let mut outfile = File::create(&outpath)?;
46
+ std::io::copy(&mut file, &mut outfile)?;
47
+ }
48
+ }
49
+
50
+ // Clear the temporary folder after extraction
51
+ if zip_path.exists() {
52
+ std::fs::remove_file(zip_path)?;
53
+ }
54
+
55
+ Ok(())
56
+ }
package/rust/main.rs CHANGED
@@ -4,24 +4,33 @@ pub mod core;
4
4
  pub mod cli;
5
5
  pub mod utils;
6
6
  pub mod config;
7
+ pub mod common;
8
+ pub mod installer;
7
9
 
8
10
  use std::io;
9
- use cli::{ Cli };
10
11
  use clap::Parser;
11
12
  use crate::{
12
13
  cli::{
14
+ bank::{
15
+ handle_bank_available_command,
16
+ handle_bank_info_command,
17
+ handle_bank_list_command,
18
+ handle_remove_bank_command, handle_update_bank_command,
19
+ },
13
20
  build::handle_build_command,
14
21
  check::handle_check_command,
22
+ driver::{ BankCommand, Cli, Commands, InstallCommand, TemplateCommand },
15
23
  init::handle_init_command,
24
+ install::handle_install_bank_command,
16
25
  play::handle_play_command,
17
26
  template::{ handle_template_info_command, handle_template_list_command },
18
- Commands,
19
- TemplateCommand,
27
+ update::handle_update_command,
20
28
  },
21
- config::{ loader::load_config, Config },
29
+ config::{ driver::Config, loader::load_config },
22
30
  };
23
31
 
24
- fn main() -> io::Result<()> {
32
+ #[tokio::main]
33
+ async fn main() -> io::Result<()> {
25
34
  let cli: Cli = Cli::parse();
26
35
  let mut config: Option<Config> = None;
27
36
 
@@ -58,6 +67,54 @@ fn main() -> io::Result<()> {
58
67
  handle_play_command(config, entry, output, watch, repeat);
59
68
  }
60
69
 
70
+ Commands::Install { command } =>
71
+ match command {
72
+ InstallCommand::Bank { name } => {
73
+ if let Err(err) = handle_install_bank_command(name).await {
74
+ eprintln!("❌ Failed to install bank: {}", err);
75
+ }
76
+ }
77
+ }
78
+
79
+ Commands::Bank { command } =>
80
+ match command {
81
+ BankCommand::List => {
82
+ if let Err(err) = handle_bank_list_command().await {
83
+ eprintln!("❌ Failed to list local banks: {}", err);
84
+ }
85
+ }
86
+
87
+ BankCommand::Available => {
88
+ if let Err(err) = handle_bank_available_command().await {
89
+ eprintln!("❌ Failed to list available banks: {}", err);
90
+ }
91
+ }
92
+
93
+ BankCommand::Info { name } => {
94
+ if let Err(err) = handle_bank_info_command(name).await {
95
+ eprintln!("❌ Failed to get bank info: {}", err);
96
+ }
97
+ }
98
+
99
+ BankCommand::Remove { name } => {
100
+ if let Err(err) = handle_remove_bank_command(name).await {
101
+ eprintln!("❌ Failed to remove bank: {}", err);
102
+ }
103
+ }
104
+
105
+ BankCommand::Update { name } => {
106
+ if let Err(err) = handle_update_bank_command(name).await {
107
+ eprintln!("❌ Failed to update bank: {}", err);
108
+ }
109
+ }
110
+ }
111
+
112
+ Commands::Update { only } => {
113
+ if let Err(err) = handle_update_command(only).await {
114
+ eprintln!("❌ Update failed: {}", err);
115
+ }
116
+ }
117
+
61
118
  _ => {}
62
119
  }
63
120