@devaloop/devalang 0.0.1-beta.2 → 0.0.1-beta.3

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 (159) hide show
  1. package/Cargo.toml +84 -81
  2. package/README.md +3 -2
  3. package/docs/CHANGELOG.md +41 -0
  4. package/docs/ROADMAP.md +3 -3
  5. package/examples/chain.deva +19 -0
  6. package/examples/plugin.deva +10 -10
  7. package/examples/routing.deva +23 -0
  8. package/out-tsc/bin/project-version.json +6 -0
  9. package/out-tsc/pkg/devalang_core_bg.wasm.d.ts +8 -8
  10. package/out-tsc/scripts/version/copy-to-binary.d.ts +1 -0
  11. package/out-tsc/scripts/version/copy-to-binary.js +79 -0
  12. package/package.json +23 -10
  13. package/project-version.json +3 -3
  14. package/rust/bindings/Cargo.toml +9 -0
  15. package/rust/bindings/src/lib.rs +86 -0
  16. package/rust/cli/addon/commands.rs +35 -0
  17. package/rust/cli/addon/download.rs +234 -0
  18. package/rust/cli/addon/install.rs +33 -0
  19. package/rust/cli/addon/list.rs +224 -0
  20. package/rust/cli/addon/metadata.rs +124 -0
  21. package/rust/cli/addon/mod.rs +8 -0
  22. package/rust/cli/addon/remove.rs +271 -0
  23. package/rust/cli/addon/update.rs +305 -0
  24. package/rust/cli/{install/addon.rs → addon/utils.rs} +109 -118
  25. package/rust/cli/build/commands.rs +153 -153
  26. package/rust/cli/build/process.rs +165 -165
  27. package/rust/cli/check/mod.rs +208 -208
  28. package/rust/cli/discover/commands.rs +275 -253
  29. package/rust/cli/discover/config.rs +109 -111
  30. package/rust/cli/discover/fs.rs +19 -19
  31. package/rust/cli/discover/install.rs +214 -103
  32. package/rust/cli/discover/metadata.rs +48 -48
  33. package/rust/cli/discover/mod.rs +5 -5
  34. package/rust/cli/me/commands.rs +52 -0
  35. package/rust/cli/me/mod.rs +1 -0
  36. package/rust/cli/mod.rs +12 -12
  37. package/rust/cli/parser.rs +30 -69
  38. package/rust/cli/play/commands.rs +375 -375
  39. package/rust/cli/play/process.rs +159 -159
  40. package/rust/core/audio/engine/driver.rs +19 -2
  41. package/rust/core/audio/engine/export.rs +169 -169
  42. package/rust/core/audio/engine/mod.rs +56 -56
  43. package/rust/core/audio/engine/notes/dsp.rs +88 -85
  44. package/rust/core/audio/engine/notes/mod.rs +53 -44
  45. package/rust/core/audio/engine/notes/params.rs +294 -294
  46. package/rust/core/audio/engine/sample/insert.rs +148 -47
  47. package/rust/core/audio/engine/sample/mod.rs +40 -40
  48. package/rust/core/audio/engine/sample/padding.rs +170 -170
  49. package/rust/core/audio/evaluator/condition.rs +61 -61
  50. package/rust/core/audio/evaluator/numeric.rs +152 -152
  51. package/rust/core/audio/evaluator/rhs.rs +16 -16
  52. package/rust/core/audio/evaluator/string_expr.rs +94 -94
  53. package/rust/core/audio/interpreter/driver.rs +574 -574
  54. package/rust/core/audio/interpreter/mod.rs +2 -2
  55. package/rust/core/audio/interpreter/statements/arrow_call/interprete.rs +9 -5
  56. package/rust/core/audio/interpreter/statements/arrow_call/methods/chord.rs +398 -384
  57. package/rust/core/audio/interpreter/statements/arrow_call/methods/effects.rs +323 -0
  58. package/rust/core/audio/interpreter/statements/arrow_call/methods/mod.rs +1 -0
  59. package/rust/core/audio/interpreter/statements/arrow_call/methods/note.rs +66 -11
  60. package/rust/core/audio/interpreter/statements/arrow_call/mod.rs +3 -3
  61. package/rust/core/audio/interpreter/statements/arrow_call/types/arp.rs +192 -192
  62. package/rust/core/audio/interpreter/statements/arrow_call/types/mod.rs +24 -24
  63. package/rust/core/audio/interpreter/statements/arrow_call/types/pad.rs +116 -116
  64. package/rust/core/audio/interpreter/statements/arrow_call/types/pluck.rs +97 -97
  65. package/rust/core/audio/interpreter/statements/arrow_call/types/sub.rs +100 -100
  66. package/rust/core/audio/interpreter/statements/automate.rs +16 -16
  67. package/rust/core/audio/interpreter/statements/call.rs +31 -1
  68. package/rust/core/audio/interpreter/statements/condition.rs +72 -72
  69. package/rust/core/audio/interpreter/statements/function.rs +24 -24
  70. package/rust/core/audio/interpreter/statements/let_.rs +36 -36
  71. package/rust/core/audio/interpreter/statements/load.rs +17 -17
  72. package/rust/core/audio/interpreter/statements/loop_.rs +115 -115
  73. package/rust/core/audio/interpreter/statements/spawn.rs +51 -2
  74. package/rust/core/audio/interpreter/statements/trigger.rs +242 -239
  75. package/rust/core/audio/loader/trigger.rs +98 -98
  76. package/rust/core/audio/player.rs +70 -70
  77. package/rust/core/audio/special/mod.rs +9 -9
  78. package/rust/core/builder/mod.rs +129 -129
  79. package/rust/core/debugger/lexer.rs +27 -27
  80. package/rust/core/debugger/logs.rs +52 -52
  81. package/rust/core/debugger/preprocessor.rs +27 -27
  82. package/rust/core/debugger/store.rs +38 -38
  83. package/rust/core/lexer/driver.rs +59 -59
  84. package/rust/core/lexer/handler/arrow.rs +82 -82
  85. package/rust/core/lexer/handler/at.rs +21 -21
  86. package/rust/core/lexer/handler/brace.rs +41 -41
  87. package/rust/core/lexer/handler/colon.rs +21 -21
  88. package/rust/core/lexer/handler/comment.rs +30 -30
  89. package/rust/core/lexer/handler/dot.rs +21 -21
  90. package/rust/core/lexer/handler/driver.rs +337 -337
  91. package/rust/core/lexer/handler/identifier.rs +47 -47
  92. package/rust/core/lexer/handler/indent.rs +66 -66
  93. package/rust/core/lexer/handler/mod.rs +15 -15
  94. package/rust/core/lexer/handler/newline.rs +23 -23
  95. package/rust/core/lexer/handler/number.rs +31 -31
  96. package/rust/core/lexer/handler/operator.rs +46 -46
  97. package/rust/core/lexer/handler/parenthesis.rs +41 -41
  98. package/rust/core/lexer/handler/slash.rs +21 -21
  99. package/rust/core/lexer/handler/string.rs +63 -63
  100. package/rust/core/lexer/mod.rs +3 -3
  101. package/rust/core/mod.rs +9 -9
  102. package/rust/core/parser/driver/block.rs +111 -111
  103. package/rust/core/parser/driver/cursor.rs +82 -82
  104. package/rust/core/parser/driver/driver_impl.rs +21 -1
  105. package/rust/core/parser/driver/mod.rs +6 -6
  106. package/rust/core/parser/driver/parse_array.rs +120 -120
  107. package/rust/core/parser/driver/parse_map.rs +247 -223
  108. package/rust/core/parser/driver/parser.rs +160 -160
  109. package/rust/core/parser/handler/arrow_call.rs +65 -14
  110. package/rust/core/parser/handler/identifier/synth.rs +171 -135
  111. package/rust/core/parser/handler/mod.rs +9 -9
  112. package/rust/core/parser/handler/pattern.rs +24 -1
  113. package/rust/core/plugin/loader.rs +137 -137
  114. package/rust/core/plugin/mod.rs +2 -2
  115. package/rust/core/plugin/runner/non_wasm.rs +481 -297
  116. package/rust/core/plugin/runner/wasm32.rs +1 -0
  117. package/rust/core/preprocessor/loader/inject.rs +313 -278
  118. package/rust/core/preprocessor/loader/loader_helpers.rs +110 -110
  119. package/rust/core/preprocessor/loader/mod.rs +235 -235
  120. package/rust/core/preprocessor/module.rs +55 -55
  121. package/rust/core/preprocessor/processor/handlers.rs +107 -107
  122. package/rust/core/preprocessor/resolver/bank.rs +49 -49
  123. package/rust/core/preprocessor/resolver/call.rs +124 -124
  124. package/rust/core/preprocessor/resolver/condition.rs +95 -95
  125. package/rust/core/preprocessor/resolver/driver.rs +324 -324
  126. package/rust/core/preprocessor/resolver/function.rs +69 -69
  127. package/rust/core/preprocessor/resolver/group.rs +122 -122
  128. package/rust/core/preprocessor/resolver/let_.rs +32 -32
  129. package/rust/core/preprocessor/resolver/loop_.rs +318 -318
  130. package/rust/core/preprocessor/resolver/mod.rs +16 -16
  131. package/rust/core/preprocessor/resolver/pattern.rs +95 -83
  132. package/rust/core/preprocessor/resolver/spawn.rs +99 -99
  133. package/rust/core/preprocessor/resolver/synth.rs +54 -54
  134. package/rust/core/preprocessor/resolver/tempo.rs +48 -48
  135. package/rust/core/preprocessor/resolver/trigger.rs +116 -116
  136. package/rust/core/preprocessor/resolver/value.rs +176 -176
  137. package/rust/core/store/global.rs +57 -57
  138. package/rust/lib.rs +323 -323
  139. package/rust/macros/Cargo.toml +14 -0
  140. package/rust/macros/src/lib.rs +52 -0
  141. package/rust/main.rs +311 -142
  142. package/rust/types/Cargo.toml +1 -1
  143. package/rust/types/src/addons.rs +3 -1
  144. package/rust/types/src/config.rs +1 -3
  145. package/rust/utils/Cargo.toml +5 -2
  146. package/rust/utils/src/file.rs +397 -14
  147. package/rust/utils/src/path.rs +31 -2
  148. package/rust/utils/src/version.rs +38 -7
  149. package/rust/web/auth.rs +5 -0
  150. package/rust/web/forge.rs +5 -0
  151. package/rust/web/mod.rs +5 -3
  152. package/typescript/scripts/version/copy-to-binary.ts +82 -0
  153. package/rust/cli/bank/api.rs +0 -122
  154. package/rust/cli/bank/commands.rs +0 -306
  155. package/rust/cli/bank/mod.rs +0 -29
  156. package/rust/cli/install/bank.rs +0 -72
  157. package/rust/cli/install/commands.rs +0 -35
  158. package/rust/cli/install/mod.rs +0 -4
  159. package/rust/cli/install/plugin.rs +0 -80
@@ -1,253 +1,275 @@
1
- use crate::cli::discover::config::add_addons_to_config;
2
- use crate::cli::discover::install::install_selected_addons;
3
- use devalang_types::DiscoveredAddon;
4
- use devalang_utils::{
5
- logger::{LogLevel, Logger},
6
- path as path_utils,
7
- spinner::start_spinner,
8
- };
9
-
10
- pub async fn handle_discover_command() -> Result<(), String> {
11
- let deva_dir = path_utils::ensure_deva_dir()?;
12
-
13
- // Search for addons (banks, plugins, presets, templates) in the .deva directory
14
- let valid_addons_extensions = ["devabank", "devaplugin", "devapreset", "devatemplate"];
15
-
16
- let mut addons_found = Vec::new();
17
-
18
- // Recursively walk the .deva directory and collect addon files matching
19
- // the known addon extensions. This allows discovery in nested folders.
20
- fn walk_dir_collect(base: &std::path::Path, exts: &[&str], out: &mut Vec<DiscoveredAddon>) {
21
- if let Ok(entries) = std::fs::read_dir(base) {
22
- for entry in entries.filter_map(|e| e.ok()) {
23
- let p = entry.path();
24
- if p.is_dir() {
25
- walk_dir_collect(&p, exts, out);
26
- } else if p.is_file() {
27
- if let Some(ext) = p.extension().and_then(|s| s.to_str()) {
28
- if exts.contains(&ext) {
29
- let name = p
30
- .file_stem()
31
- .map(|s| s.to_string_lossy().to_string())
32
- .unwrap_or_default();
33
- let addon_type = match ext {
34
- "devabank" => "bank",
35
- "devaplugin" => "plugin",
36
- "devapreset" => "preset",
37
- "devatemplate" => "template",
38
- _ => "unknown",
39
- };
40
-
41
- out.push(DiscoveredAddon {
42
- path: p.clone(),
43
- name,
44
- extension: ext.to_string(),
45
- addon_type: addon_type.to_string(),
46
- });
47
- }
48
- }
49
- }
50
- }
51
- }
52
- }
53
-
54
- walk_dir_collect(&deva_dir, &valid_addons_extensions, &mut addons_found);
55
-
56
- let logger = Logger::new();
57
-
58
- let banks_found = addons_found
59
- .iter()
60
- .filter(|addon| addon.addon_type == "bank")
61
- .cloned()
62
- .collect::<Vec<_>>();
63
-
64
- let plugins_found = addons_found
65
- .iter()
66
- .filter(|addon| addon.addon_type == "plugin")
67
- .cloned()
68
- .collect::<Vec<_>>();
69
-
70
- let presets_found = addons_found
71
- .iter()
72
- .filter(|addon| addon.addon_type == "preset")
73
- .cloned()
74
- .collect::<Vec<_>>();
75
-
76
- let templates_found = addons_found
77
- .iter()
78
- .filter(|addon| addon.addon_type == "template")
79
- .cloned()
80
- .collect::<Vec<_>>();
81
-
82
- let mut all_addons = Vec::with_capacity(
83
- banks_found.len() + plugins_found.len() + presets_found.len() + templates_found.len(),
84
- );
85
- all_addons.extend(banks_found.iter().cloned());
86
- all_addons.extend(plugins_found.iter().cloned());
87
- all_addons.extend(presets_found.iter().cloned());
88
- all_addons.extend(templates_found.iter().cloned());
89
-
90
- println!();
91
-
92
- if all_addons.is_empty() {
93
- logger.log_message(LogLevel::Error, "No addons found in the '.deva' folder");
94
- return Ok(());
95
- }
96
-
97
- if !banks_found.is_empty() {
98
- let mut bank_traces: Vec<String> = Vec::new();
99
-
100
- for addon in &banks_found {
101
- bank_traces.push(addon.name.to_string());
102
- }
103
-
104
- let trace_refs: Vec<&str> = bank_traces.iter().map(|s| s.as_str()).collect();
105
-
106
- logger.log_message_with_trace(
107
- LogLevel::Info,
108
- format!("Found {} compiled banks in workspace", banks_found.len()).as_str(),
109
- trace_refs,
110
- );
111
- }
112
-
113
- if !plugins_found.is_empty() {
114
- let mut plugin_traces: Vec<String> = Vec::new();
115
-
116
- for addon in &plugins_found {
117
- plugin_traces.push(addon.name.to_string());
118
- }
119
-
120
- let trace_refs: Vec<&str> = plugin_traces.iter().map(|s| s.as_str()).collect();
121
-
122
- logger.log_message_with_trace(
123
- LogLevel::Info,
124
- format!(
125
- "Found {} compiled plugins in workspace",
126
- plugins_found.len()
127
- )
128
- .as_str(),
129
- trace_refs,
130
- );
131
- }
132
-
133
- if !presets_found.is_empty() {
134
- let mut preset_traces: Vec<String> = Vec::new();
135
-
136
- for addon in &presets_found {
137
- preset_traces.push(addon.name.to_string());
138
- }
139
-
140
- let trace_refs: Vec<&str> = preset_traces.iter().map(|s| s.as_str()).collect();
141
-
142
- logger.log_message_with_trace(
143
- LogLevel::Info,
144
- format!(
145
- "Found {} compiled presets in workspace",
146
- presets_found.len()
147
- )
148
- .as_str(),
149
- trace_refs,
150
- );
151
- }
152
-
153
- if !templates_found.is_empty() {
154
- let mut template_traces: Vec<String> = Vec::new();
155
-
156
- for addon in &templates_found {
157
- template_traces.push(addon.name.to_string());
158
- }
159
-
160
- let trace_refs: Vec<&str> = template_traces.iter().map(|s| s.as_str()).collect();
161
-
162
- logger.log_message_with_trace(
163
- LogLevel::Info,
164
- format!(
165
- "Found {} compiled templates in workspace",
166
- templates_found.len()
167
- )
168
- .as_str(),
169
- trace_refs,
170
- );
171
- }
172
-
173
- println!();
174
-
175
- // Build user-friendly, unique labels tied to each addon by including the path
176
- let choice_labels: Vec<String> = all_addons
177
- .iter()
178
- .map(|addon| {
179
- format!(
180
- "{}: {} ({})",
181
- addon.addon_type,
182
- addon.name,
183
- addon.path.display()
184
- )
185
- })
186
- .collect();
187
-
188
- let selected_addons = match inquire::MultiSelect::new(
189
- "Select addons to install:",
190
- choice_labels.clone(),
191
- )
192
- .prompt()
193
- {
194
- Ok(selected_addons) => selected_addons,
195
- Err(err) => {
196
- logger.log_message(
197
- LogLevel::Error,
198
- format!("Error selecting addons: {}", err).as_str(),
199
- );
200
-
201
- return Err(format!("Error selecting addons: {}", err));
202
- }
203
- };
204
-
205
- let spinner = start_spinner("Installing addons...");
206
-
207
- let addons_to_install = selected_addons
208
- .iter()
209
- .filter_map(|label| {
210
- all_addons.iter().find(|addon| {
211
- let candidate = format!(
212
- "{}: {} ({})",
213
- addon.addon_type,
214
- addon.name,
215
- addon.path.display()
216
- );
217
- &candidate == label
218
- })
219
- })
220
- .cloned()
221
- .collect::<Vec<_>>();
222
-
223
- let install_selected_addons_result = install_selected_addons(addons_to_install).await;
224
- match install_selected_addons_result {
225
- Ok(addons_enriched) => {
226
- if let Err(e) = add_addons_to_config(addons_enriched).await {
227
- spinner.finish_and_clear();
228
- logger.log_message(
229
- LogLevel::Error,
230
- format!("Failed to add addons to config: {}", e).as_str(),
231
- );
232
- return Err(format!("Failed to add addons to config: {}", e));
233
- }
234
-
235
- spinner.finish_and_clear();
236
- println!();
237
- logger.log_message(
238
- LogLevel::Success,
239
- "Successfully installed addons !".to_string().as_str(),
240
- );
241
- }
242
- Err(e) => {
243
- spinner.finish_and_clear();
244
- logger.log_message(
245
- LogLevel::Error,
246
- format!("Failed to install addons: {}", e).as_str(),
247
- );
248
- return Err(format!("Failed to install addons: {}", e));
249
- }
250
- }
251
-
252
- Ok(())
253
- }
1
+ use crate::cli::discover::config::add_addons_to_config;
2
+ use crate::cli::discover::install::install_selected_addons;
3
+ use devalang_types::DiscoveredAddon;
4
+ use devalang_utils::spinner::start_spinner;
5
+ use devalang_utils::{
6
+ logger::{LogLevel, Logger},
7
+ path as path_utils,
8
+ };
9
+
10
+ pub async fn handle_discover_command(no_clear_tmp: bool) -> Result<(), String> {
11
+ let deva_dir = path_utils::ensure_deva_dir()?;
12
+
13
+ // Search for compiled addon archives in the .deva directory
14
+ // New norm: archives are packaged as `.tar.gz`. We recursively find all
15
+ // files ending with `.tar.gz` and propose them for installation. The
16
+ // install flow will inspect the extracted content to determine the exact
17
+ // addon type (bank/plugin/preset/template) when possible.
18
+
19
+ let mut addons_found = Vec::new();
20
+
21
+ fn walk_dir_collect_tar_gz(base: &std::path::Path, out: &mut Vec<DiscoveredAddon>) {
22
+ if let Ok(entries) = std::fs::read_dir(base) {
23
+ for entry in entries.filter_map(|e| e.ok()) {
24
+ let p = entry.path();
25
+ if p.is_dir() {
26
+ walk_dir_collect_tar_gz(&p, out);
27
+ } else if p.is_file() {
28
+ let name = p.file_name().and_then(|s| s.to_str()).unwrap_or("");
29
+ if name.ends_with(".tar.gz") || name.ends_with(".tgz") {
30
+ // derive a friendly name (strip extensions)
31
+ let stem = if name.ends_with(".tar.gz") {
32
+ name.trim_end_matches(".tar.gz").to_string()
33
+ } else {
34
+ name.trim_end_matches(".tgz").to_string()
35
+ };
36
+
37
+ let publisher = p
38
+ .parent()
39
+ .and_then(|parent| parent.file_name())
40
+ .and_then(|s| s.to_str())
41
+ .unwrap_or("unknown")
42
+ .to_string();
43
+
44
+ out.push(DiscoveredAddon {
45
+ name: stem,
46
+ path: p.clone(),
47
+ publisher: publisher.clone(),
48
+ extension: "tar.gz".to_string(),
49
+ addon_type: "unknown".to_string(),
50
+ });
51
+ }
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ walk_dir_collect_tar_gz(&deva_dir, &mut addons_found);
58
+
59
+ // Pre-classify discovered archives by inspecting their contents for metadata
60
+ for addon in addons_found.iter_mut() {
61
+ if let Ok(t) = devalang_utils::file::detect_addon_type_in_archive(&addon.path) {
62
+ if t != "unknown" {
63
+ addon.addon_type = t;
64
+ }
65
+ }
66
+ }
67
+
68
+ let logger = Logger::new();
69
+
70
+ let banks_found = addons_found
71
+ .iter()
72
+ .filter(|addon| addon.addon_type == "bank")
73
+ .cloned()
74
+ .collect::<Vec<_>>();
75
+
76
+ let plugins_found = addons_found
77
+ .iter()
78
+ .filter(|addon| addon.addon_type == "plugin")
79
+ .cloned()
80
+ .collect::<Vec<_>>();
81
+
82
+ let presets_found = addons_found
83
+ .iter()
84
+ .filter(|addon| addon.addon_type == "preset")
85
+ .cloned()
86
+ .collect::<Vec<_>>();
87
+
88
+ let templates_found = addons_found
89
+ .iter()
90
+ .filter(|addon| addon.addon_type == "template")
91
+ .cloned()
92
+ .collect::<Vec<_>>();
93
+
94
+ // Combine discovered known-type addons. If nothing was classified (all
95
+ // entries have `addon_type == "unknown"`), fall back to the raw
96
+ // `addons_found` so users can still install archives discovered in
97
+ // `.deva`.
98
+ let mut all_addons = Vec::with_capacity(
99
+ banks_found.len() + plugins_found.len() + presets_found.len() + templates_found.len(),
100
+ );
101
+ all_addons.extend(banks_found.iter().cloned());
102
+ all_addons.extend(plugins_found.iter().cloned());
103
+ all_addons.extend(presets_found.iter().cloned());
104
+ all_addons.extend(templates_found.iter().cloned());
105
+
106
+ if all_addons.is_empty() {
107
+ // No known types detected — include everything we discovered.
108
+ all_addons = addons_found.clone();
109
+ }
110
+
111
+ println!();
112
+
113
+ if all_addons.is_empty() {
114
+ logger.log_message(LogLevel::Error, "No addons found in the '.deva' folder");
115
+ return Ok(());
116
+ }
117
+
118
+ if !banks_found.is_empty() {
119
+ let mut bank_traces: Vec<String> = Vec::new();
120
+
121
+ for addon in &banks_found {
122
+ bank_traces.push(addon.name.to_string());
123
+ }
124
+
125
+ let trace_refs: Vec<&str> = bank_traces.iter().map(|s| s.as_str()).collect();
126
+
127
+ logger.log_message_with_trace(
128
+ LogLevel::Info,
129
+ format!("Found {} compiled banks in workspace", banks_found.len()).as_str(),
130
+ trace_refs,
131
+ );
132
+ }
133
+
134
+ if !plugins_found.is_empty() {
135
+ let mut plugin_traces: Vec<String> = Vec::new();
136
+
137
+ for addon in &plugins_found {
138
+ plugin_traces.push(addon.name.to_string());
139
+ }
140
+
141
+ let trace_refs: Vec<&str> = plugin_traces.iter().map(|s| s.as_str()).collect();
142
+
143
+ logger.log_message_with_trace(
144
+ LogLevel::Info,
145
+ format!(
146
+ "Found {} compiled plugins in workspace",
147
+ plugins_found.len()
148
+ )
149
+ .as_str(),
150
+ trace_refs,
151
+ );
152
+ }
153
+
154
+ if !presets_found.is_empty() {
155
+ let mut preset_traces: Vec<String> = Vec::new();
156
+
157
+ for addon in &presets_found {
158
+ preset_traces.push(addon.name.to_string());
159
+ }
160
+
161
+ let trace_refs: Vec<&str> = preset_traces.iter().map(|s| s.as_str()).collect();
162
+
163
+ logger.log_message_with_trace(
164
+ LogLevel::Info,
165
+ format!(
166
+ "Found {} compiled presets in workspace",
167
+ presets_found.len()
168
+ )
169
+ .as_str(),
170
+ trace_refs,
171
+ );
172
+ }
173
+
174
+ if !templates_found.is_empty() {
175
+ let mut template_traces: Vec<String> = Vec::new();
176
+
177
+ for addon in &templates_found {
178
+ template_traces.push(addon.name.to_string());
179
+ }
180
+
181
+ let trace_refs: Vec<&str> = template_traces.iter().map(|s| s.as_str()).collect();
182
+
183
+ logger.log_message_with_trace(
184
+ LogLevel::Info,
185
+ format!(
186
+ "Found {} compiled templates in workspace",
187
+ templates_found.len()
188
+ )
189
+ .as_str(),
190
+ trace_refs,
191
+ );
192
+ }
193
+
194
+ println!();
195
+
196
+ // Build user-friendly, unique labels tied to each addon by including the path
197
+ let choice_labels: Vec<String> = all_addons
198
+ .iter()
199
+ .map(|addon| {
200
+ format!(
201
+ "{}: {} ({})",
202
+ addon.addon_type,
203
+ addon.name,
204
+ addon.path.display()
205
+ )
206
+ })
207
+ .collect();
208
+
209
+ let selected_addons = match inquire::MultiSelect::new(
210
+ "Select addons to install:",
211
+ choice_labels.clone(),
212
+ )
213
+ .prompt()
214
+ {
215
+ Ok(selected_addons) => selected_addons,
216
+ Err(err) => {
217
+ logger.log_message(
218
+ LogLevel::Error,
219
+ format!("Error selecting addons: {}", err).as_str(),
220
+ );
221
+
222
+ return Err(format!("Error selecting addons: {}", err));
223
+ }
224
+ };
225
+
226
+ let spinner = start_spinner("Installing addons...");
227
+
228
+ let addons_to_install = selected_addons
229
+ .iter()
230
+ .filter_map(|label| {
231
+ all_addons.iter().find(|addon| {
232
+ let candidate = format!(
233
+ "{}: {} ({})",
234
+ addon.addon_type,
235
+ addon.name,
236
+ addon.path.display()
237
+ );
238
+ &candidate == label
239
+ })
240
+ })
241
+ .cloned()
242
+ .collect::<Vec<_>>();
243
+
244
+ let install_selected_addons_result =
245
+ install_selected_addons(addons_to_install, no_clear_tmp).await;
246
+ match install_selected_addons_result {
247
+ Ok(addons_enriched) => {
248
+ if let Err(e) = add_addons_to_config(addons_enriched).await {
249
+ spinner.finish_and_clear();
250
+ logger.log_message(
251
+ LogLevel::Error,
252
+ format!("Failed to add addons to config: {}", e).as_str(),
253
+ );
254
+ return Err(format!("Failed to add addons to config: {}", e));
255
+ }
256
+
257
+ spinner.finish_and_clear();
258
+ println!();
259
+ logger.log_message(
260
+ LogLevel::Success,
261
+ "Successfully installed addons !".to_string().as_str(),
262
+ );
263
+ }
264
+ Err(e) => {
265
+ spinner.finish_and_clear();
266
+ logger.log_message(
267
+ LogLevel::Error,
268
+ format!("Failed to install addons: {}", e).as_str(),
269
+ );
270
+ return Err(format!("Failed to install addons: {}", e));
271
+ }
272
+ }
273
+
274
+ Ok(())
275
+ }