@devaloop/devalang 0.0.1-alpha.16-hotfix.3 → 0.0.1-alpha.18

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 (239) hide show
  1. package/.cargo/config.toml +2 -0
  2. package/.devalang +10 -10
  3. package/.github/workflows/ci.yml +0 -1
  4. package/Cargo.toml +18 -2
  5. package/README.md +82 -34
  6. package/docs/CHANGELOG.md +91 -0
  7. package/docs/ROADMAP.md +7 -4
  8. package/docs/TODO.md +1 -1
  9. package/examples/index.deva +55 -35
  10. package/examples/pattern.deva +5 -5
  11. package/out-tsc/bin/index.d.ts +2 -0
  12. package/out-tsc/core/functions/index.d.ts +37 -0
  13. package/out-tsc/core/functions/index.js +76 -0
  14. package/out-tsc/core/index.d.ts +6 -0
  15. package/out-tsc/core/index.js +22 -0
  16. package/out-tsc/core/types/index.d.ts +4 -0
  17. package/out-tsc/core/types/index.js +20 -0
  18. package/out-tsc/core/types/plugin.d.ts +18 -0
  19. package/out-tsc/core/types/plugin.js +2 -0
  20. package/out-tsc/core/types/result.d.ts +27 -0
  21. package/out-tsc/core/types/result.js +2 -0
  22. package/out-tsc/core/types/statement.d.ts +106 -0
  23. package/out-tsc/core/types/statement.js +2 -0
  24. package/out-tsc/core/types/value.d.ts +43 -0
  25. package/out-tsc/core/types/value.js +2 -0
  26. package/out-tsc/index.d.ts +7 -0
  27. package/out-tsc/index.js +41 -2
  28. package/out-tsc/pkg/devalang_core.d.ts +7 -0
  29. package/out-tsc/pkg/devalang_core_bg.wasm.d.ts +33 -0
  30. package/out-tsc/scripts/copy-wasm-dts.d.ts +1 -0
  31. package/out-tsc/scripts/copy-wasm-dts.js +73 -0
  32. package/out-tsc/scripts/postinstall.d.ts +1 -0
  33. package/out-tsc/scripts/postinstall.js +33 -23
  34. package/out-tsc/scripts/version/bump.d.ts +1 -0
  35. package/out-tsc/scripts/version/fetch.d.ts +1 -0
  36. package/out-tsc/scripts/version/index.d.ts +1 -0
  37. package/out-tsc/scripts/version/sync.d.ts +1 -0
  38. package/package.json +16 -4
  39. package/project-version.json +3 -3
  40. package/rust/cli/bank/api.rs +122 -0
  41. package/rust/cli/bank/commands.rs +275 -0
  42. package/rust/cli/bank/mod.rs +29 -0
  43. package/rust/cli/build/commands.rs +107 -0
  44. package/rust/cli/build/mod.rs +2 -0
  45. package/rust/cli/build/process.rs +146 -0
  46. package/rust/cli/{check.rs → check/mod.rs} +18 -31
  47. package/rust/cli/discover/commands.rs +253 -0
  48. package/rust/cli/discover/config.rs +111 -0
  49. package/rust/cli/discover/fs.rs +19 -0
  50. package/rust/cli/discover/install.rs +103 -0
  51. package/rust/cli/discover/metadata.rs +48 -0
  52. package/rust/cli/discover/mod.rs +5 -0
  53. package/rust/cli/{init.rs → init/commands.rs} +88 -87
  54. package/rust/cli/init/mod.rs +1 -0
  55. package/rust/cli/install/addon.rs +126 -0
  56. package/rust/cli/install/bank.rs +53 -0
  57. package/rust/cli/{install.rs → install/commands.rs} +9 -9
  58. package/rust/{installer → cli/install}/mod.rs +2 -3
  59. package/rust/cli/install/plugin.rs +61 -0
  60. package/rust/cli/{login.rs → login/commands.rs} +8 -11
  61. package/rust/cli/login/mod.rs +1 -0
  62. package/rust/cli/mod.rs +2 -2
  63. package/rust/cli/{driver.rs → parser.rs} +7 -2
  64. package/rust/cli/play/commands.rs +324 -0
  65. package/rust/cli/play/io.rs +17 -0
  66. package/rust/cli/play/mod.rs +5 -0
  67. package/rust/cli/play/process.rs +150 -0
  68. package/rust/cli/play/realtime.rs +91 -0
  69. package/rust/cli/play/utils.rs +23 -0
  70. package/rust/cli/{telemetry.rs → telemetry/commands.rs} +4 -4
  71. package/rust/cli/telemetry/event_creator.rs +80 -0
  72. package/rust/cli/telemetry/mod.rs +3 -0
  73. package/rust/cli/telemetry/send.rs +51 -0
  74. package/rust/cli/{template.rs → template/commands.rs} +1 -1
  75. package/rust/cli/template/mod.rs +1 -0
  76. package/rust/cli/{update.rs → update/commands.rs} +6 -6
  77. package/rust/cli/update/mod.rs +1 -0
  78. package/rust/config/driver.rs +57 -72
  79. package/rust/config/mod.rs +1 -2
  80. package/rust/config/ops.rs +26 -0
  81. package/rust/config/settings.rs +40 -42
  82. package/rust/core/audio/engine/helpers.rs +158 -0
  83. package/rust/core/audio/engine/mod.rs +7 -0
  84. package/rust/core/audio/engine/sample.rs +359 -0
  85. package/rust/core/audio/engine/synth.rs +325 -0
  86. package/rust/core/audio/evaluator.rs +68 -27
  87. package/rust/core/audio/interpreter/arrow_call.rs +113 -33
  88. package/rust/core/audio/interpreter/call.rs +232 -56
  89. package/rust/core/audio/interpreter/condition.rs +3 -2
  90. package/rust/core/audio/interpreter/driver.rs +206 -151
  91. package/rust/core/audio/interpreter/let_.rs +1 -1
  92. package/rust/core/audio/interpreter/load.rs +2 -1
  93. package/rust/core/audio/interpreter/loop_.rs +7 -6
  94. package/rust/core/audio/interpreter/sleep.rs +2 -1
  95. package/rust/core/audio/interpreter/spawn.rs +186 -54
  96. package/rust/core/audio/interpreter/tempo.rs +31 -10
  97. package/rust/core/audio/interpreter/trigger.rs +2 -2
  98. package/rust/core/audio/loader/trigger.rs +4 -7
  99. package/rust/core/audio/player.rs +6 -0
  100. package/rust/core/audio/renderer.rs +5 -7
  101. package/rust/core/audio/special/env.rs +3 -1
  102. package/rust/core/audio/special/math.rs +26 -6
  103. package/rust/core/audio/special/modulator.rs +2 -2
  104. package/rust/core/builder/mod.rs +9 -3
  105. package/rust/core/debugger/lexer.rs +1 -1
  106. package/rust/core/debugger/mod.rs +6 -0
  107. package/rust/core/debugger/module.rs +4 -4
  108. package/rust/core/debugger/preprocessor.rs +1 -1
  109. package/rust/core/debugger/store.rs +2 -2
  110. package/rust/core/error/mod.rs +189 -0
  111. package/rust/core/lexer/driver.rs +61 -0
  112. package/rust/core/lexer/handler/arrow.rs +1 -1
  113. package/rust/core/lexer/handler/at.rs +1 -1
  114. package/rust/core/lexer/handler/brace.rs +2 -2
  115. package/rust/core/lexer/handler/colon.rs +1 -1
  116. package/rust/core/lexer/handler/comment.rs +1 -1
  117. package/rust/core/lexer/handler/dot.rs +1 -1
  118. package/rust/core/lexer/handler/driver.rs +1 -1
  119. package/rust/core/lexer/handler/identifier.rs +4 -3
  120. package/rust/core/lexer/handler/mod.rs +1 -2
  121. package/rust/core/lexer/handler/number.rs +1 -1
  122. package/rust/core/lexer/handler/operator.rs +1 -1
  123. package/rust/core/lexer/handler/parenthesis.rs +2 -2
  124. package/rust/core/lexer/handler/slash.rs +1 -1
  125. package/rust/core/lexer/handler/string.rs +1 -1
  126. package/rust/core/lexer/mod.rs +1 -52
  127. package/rust/core/lexer/token.rs +91 -97
  128. package/rust/core/mod.rs +0 -1
  129. package/rust/core/parser/driver.rs +78 -22
  130. package/rust/core/parser/handler/arrow_call.rs +28 -8
  131. package/rust/core/parser/handler/at.rs +55 -21
  132. package/rust/core/parser/handler/bank.rs +14 -4
  133. package/rust/core/parser/handler/condition.rs +6 -3
  134. package/rust/core/parser/handler/dot.rs +5 -3
  135. package/rust/core/parser/handler/identifier/automate.rs +13 -16
  136. package/rust/core/parser/handler/identifier/call.rs +4 -4
  137. package/rust/core/parser/handler/identifier/emit.rs +9 -5
  138. package/rust/core/parser/handler/identifier/function.rs +20 -7
  139. package/rust/core/parser/handler/identifier/group.rs +11 -7
  140. package/rust/core/parser/handler/identifier/let_.rs +24 -9
  141. package/rust/core/parser/handler/identifier/mod.rs +6 -5
  142. package/rust/core/parser/handler/identifier/on.rs +16 -7
  143. package/rust/core/parser/handler/identifier/print.rs +6 -9
  144. package/rust/core/parser/handler/identifier/sleep.rs +12 -5
  145. package/rust/core/parser/handler/identifier/spawn.rs +4 -4
  146. package/rust/core/parser/handler/identifier/synth.rs +79 -9
  147. package/rust/core/parser/handler/loop_.rs +38 -13
  148. package/rust/core/parser/handler/mod.rs +1 -0
  149. package/rust/core/parser/handler/pattern.rs +74 -0
  150. package/rust/core/parser/handler/tempo.rs +9 -5
  151. package/rust/core/parser/mod.rs +0 -1
  152. package/rust/core/parser/statement.rs +6 -137
  153. package/rust/core/plugin/loader.rs +41 -27
  154. package/rust/core/plugin/runner.rs +68 -17
  155. package/rust/core/preprocessor/loader.rs +181 -99
  156. package/rust/core/preprocessor/processor.rs +9 -9
  157. package/rust/core/preprocessor/resolver/bank.rs +6 -8
  158. package/rust/core/preprocessor/resolver/call.rs +47 -23
  159. package/rust/core/preprocessor/resolver/condition.rs +6 -8
  160. package/rust/core/preprocessor/resolver/driver.rs +28 -28
  161. package/rust/core/preprocessor/resolver/function.rs +6 -6
  162. package/rust/core/preprocessor/resolver/group.rs +6 -8
  163. package/rust/core/preprocessor/resolver/loop_.rs +8 -10
  164. package/rust/core/preprocessor/resolver/mod.rs +1 -0
  165. package/rust/core/preprocessor/resolver/pattern.rs +75 -0
  166. package/rust/core/preprocessor/resolver/spawn.rs +45 -22
  167. package/rust/core/preprocessor/resolver/synth.rs +6 -8
  168. package/rust/core/preprocessor/resolver/tempo.rs +6 -8
  169. package/rust/core/preprocessor/resolver/trigger.rs +22 -19
  170. package/rust/core/preprocessor/resolver/value.rs +99 -4
  171. package/rust/core/store/export.rs +28 -28
  172. package/rust/core/store/function.rs +6 -0
  173. package/rust/core/store/global.rs +7 -1
  174. package/rust/core/store/import.rs +28 -28
  175. package/rust/core/store/variable.rs +16 -2
  176. package/rust/core/utils/mod.rs +0 -1
  177. package/rust/lib.rs +102 -9
  178. package/rust/main.rs +159 -45
  179. package/rust/types/Cargo.toml +11 -0
  180. package/rust/types/src/addons.rs +55 -0
  181. package/rust/types/src/ast.rs +202 -0
  182. package/rust/types/src/config.rs +74 -0
  183. package/rust/types/src/lib.rs +12 -0
  184. package/rust/types/src/telemetry.rs +85 -0
  185. package/rust/utils/Cargo.toml +26 -0
  186. package/rust/utils/{error.rs → src/error.rs} +186 -200
  187. package/rust/utils/src/file.rs +94 -0
  188. package/rust/utils/src/first_usage.rs +97 -0
  189. package/rust/utils/{mod.rs → src/lib.rs} +1 -1
  190. package/rust/utils/{logger.rs → src/logger.rs} +17 -12
  191. package/rust/utils/src/path.rs +88 -0
  192. package/rust/utils/src/signature.rs +41 -0
  193. package/rust/utils/{spinner.rs → src/spinner.rs} +3 -5
  194. package/rust/utils/src/version.rs +27 -0
  195. package/rust/utils/{watcher.rs → src/watcher.rs} +13 -1
  196. package/rust/web/cdn.rs +34 -0
  197. package/templates/minimal/README.md +98 -54
  198. package/templates/welcome/README.md +98 -54
  199. package/templates/welcome/src/index.deva +56 -8
  200. package/templates/welcome/src/variables.deva +2 -4
  201. package/tests/rust/TODO.md +0 -0
  202. package/tests/typescript/index.spec.ts +136 -0
  203. package/tests/typescript/playhead.spec.ts +36 -0
  204. package/tests/typescript/render_e2e.spec.ts +77 -0
  205. package/tsconfig.json +1 -1
  206. package/typescript/core/functions/index.ts +83 -0
  207. package/typescript/core/index.ts +6 -0
  208. package/typescript/core/types/index.ts +4 -0
  209. package/typescript/core/types/plugin.ts +19 -0
  210. package/typescript/core/types/result.ts +29 -0
  211. package/typescript/core/types/statement.ts +47 -0
  212. package/typescript/core/types/value.ts +29 -0
  213. package/typescript/index.ts +7 -2
  214. package/typescript/pkg/devalang_core.d.ts +4 -0
  215. package/typescript/scripts/copy-wasm-dts.ts +41 -0
  216. package/rust/cli/bank.rs +0 -462
  217. package/rust/cli/build.rs +0 -252
  218. package/rust/cli/play.rs +0 -1123
  219. package/rust/common/cdn.rs +0 -5
  220. package/rust/config/loader.rs +0 -165
  221. package/rust/config/stats.rs +0 -257
  222. package/rust/core/audio/engine.rs +0 -696
  223. package/rust/core/shared/bank.rs +0 -21
  224. package/rust/core/shared/duration.rs +0 -9
  225. package/rust/core/shared/mod.rs +0 -3
  226. package/rust/core/shared/value.rs +0 -35
  227. package/rust/core/utils/validation.rs +0 -35
  228. package/rust/installer/addon.rs +0 -84
  229. package/rust/installer/bank.rs +0 -62
  230. package/rust/installer/plugin.rs +0 -54
  231. package/rust/installer/utils.rs +0 -56
  232. package/rust/utils/file.rs +0 -38
  233. package/rust/utils/first_usage.rs +0 -83
  234. package/rust/utils/signature.rs +0 -19
  235. package/rust/utils/telemetry.rs +0 -292
  236. package/rust/utils/version.rs +0 -15
  237. /package/rust/{common → web}/api.rs +0 -0
  238. /package/rust/{common → web}/mod.rs +0 -0
  239. /package/rust/{common → web}/sso.rs +0 -0
@@ -11,13 +11,13 @@ use crate::{
11
11
  store::global::GlobalStore,
12
12
  utils::path::{find_entry_file, normalize_path},
13
13
  },
14
- utils::{
15
- logger::{LogLevel, Logger},
16
- spinner::with_spinner,
17
- watcher::watch_directory,
18
- },
19
14
  };
20
- use std::{thread, time::Duration};
15
+
16
+ use devalang_utils::{
17
+ logger::{LogLevel, Logger},
18
+ spinner::start_spinner,
19
+ watcher::watch_directory,
20
+ };
21
21
 
22
22
  #[cfg(feature = "cli")]
23
23
  pub fn handle_check_command(
@@ -31,18 +31,18 @@ pub fn handle_check_command(
31
31
  config
32
32
  .as_ref()
33
33
  .and_then(|c| c.defaults.entry.clone())
34
- .unwrap_or_else(|| "".to_string())
34
+ .unwrap_or_default()
35
35
  } else {
36
- entry.clone().unwrap_or_else(|| "".to_string())
36
+ entry.clone().unwrap_or_default()
37
37
  };
38
38
 
39
39
  let fetched_output = if output.is_none() {
40
40
  config
41
41
  .as_ref()
42
42
  .and_then(|c| c.defaults.output.clone())
43
- .unwrap_or_else(|| "".to_string())
43
+ .unwrap_or_default()
44
44
  } else {
45
- output.clone().unwrap_or_else(|| "".to_string())
45
+ output.clone().unwrap_or_default()
46
46
  };
47
47
 
48
48
  let fetched_watch = if watch {
@@ -124,11 +124,9 @@ fn begin_check(
124
124
  entry: String,
125
125
  output: String,
126
126
  debug: bool,
127
- config: Option<ProjectConfig>,
127
+ _config: Option<ProjectConfig>,
128
128
  ) -> Result<(), String> {
129
- let spinner = with_spinner("Checking...", || {
130
- thread::sleep(Duration::from_millis(800));
131
- });
129
+ let spinner = start_spinner("Checking...");
132
130
 
133
131
  let duration = std::time::Instant::now();
134
132
 
@@ -145,7 +143,7 @@ fn begin_check(
145
143
  // Debugging: Log loaded modules and errors
146
144
  let logger = Logger::new();
147
145
  logger.log_message(LogLevel::Info, "Loaded modules:");
148
- for (module_name, _) in &modules.0 {
146
+ for module_name in modules.0.keys() {
149
147
  logger.log_message(LogLevel::Info, &format!("- {}", module_name));
150
148
  }
151
149
 
@@ -185,10 +183,10 @@ fn begin_check(
185
183
  );
186
184
  }
187
185
 
188
- let all_errors = crate::utils::error::collect_all_errors_with_modules(&modules.1);
186
+ let all_errors = crate::core::error::collect_all_errors_with_modules(&modules.1);
189
187
 
190
- let (warnings, criticals) = crate::utils::error::partition_errors(all_errors);
191
- crate::utils::error::log_errors_with_stack("Check", &warnings, &criticals);
188
+ let (warnings, criticals) = crate::core::error::partition_errors(all_errors);
189
+ crate::core::error::log_errors_with_stack("Check", &warnings, &criticals);
192
190
 
193
191
  if !criticals.is_empty() {
194
192
  spinner.finish_and_clear();
@@ -196,18 +194,6 @@ fn begin_check(
196
194
  } else {
197
195
  logger.log_message(LogLevel::Success, "No errors detected.");
198
196
 
199
- // Compute and persist rich stats
200
- let stats = crate::config::stats::compute_from(
201
- &modules.1,
202
- &global_store,
203
- &config,
204
- Some(&normalized_output_dir),
205
- );
206
- crate::config::stats::set_memory_stats(stats.clone());
207
- if let Err(e) = crate::config::stats::save_to_file(&stats) {
208
- eprintln!("[stats] failed to save: {}", e);
209
- }
210
-
211
197
  let success_message = format!(
212
198
  "Check completed successfully in {:.2?}. Output files written to: '{}'",
213
199
  duration.elapsed(),
@@ -216,6 +202,7 @@ fn begin_check(
216
202
 
217
203
  spinner.finish_and_clear();
218
204
  logger.log_message(LogLevel::Success, &success_message);
219
- Ok(())
220
205
  }
206
+
207
+ Ok(())
221
208
  }
@@ -0,0 +1,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::{
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
+ }
@@ -0,0 +1,111 @@
1
+ use devalang_core::config::driver::ProjectConfigExt;
2
+ use devalang_types::{AddonWithMetadata, ProjectConfigBankEntry, ProjectConfigPluginEntry};
3
+ use devalang_utils::path as path_utils;
4
+
5
+ pub async fn add_addons_to_config(addons: Vec<AddonWithMetadata>) -> Result<(), String> {
6
+ let config_path = path_utils::get_devalang_config_path()?;
7
+ let mut config = crate::config::ops::load_config(Some(&config_path))
8
+ .ok_or_else(|| format!("Failed to load config from '{}'", config_path.display()))?;
9
+
10
+ for addon in addons {
11
+ let addon_path_as_devalang_protocol = format!(
12
+ "devalang://{}/{}.{}",
13
+ addon.addon_type, addon.metadata.author, addon.metadata.name
14
+ );
15
+
16
+ match addon.addon_type.as_str() {
17
+ "bank" => {
18
+ if config.banks.is_none() {
19
+ config.banks = Some(Vec::new());
20
+ }
21
+
22
+ let banks = config.banks.as_mut().unwrap();
23
+
24
+ let exists = banks
25
+ .iter()
26
+ .any(|b| b.path == addon_path_as_devalang_protocol);
27
+ if exists {
28
+ println!("Bank '{}' already in config", addon.name);
29
+ continue;
30
+ }
31
+
32
+ banks.push(ProjectConfigBankEntry {
33
+ path: addon_path_as_devalang_protocol,
34
+ version: Some(addon.metadata.version.clone()),
35
+ });
36
+ }
37
+
38
+ "plugin" => {
39
+ if config.plugins.is_none() {
40
+ config.plugins = Some(Vec::new());
41
+ }
42
+
43
+ let plugins = config.plugins.as_mut().unwrap();
44
+
45
+ let exists = plugins
46
+ .iter()
47
+ .any(|p| p.path == addon_path_as_devalang_protocol);
48
+ if exists {
49
+ println!("Plugin '{}' already in config", addon.name);
50
+ continue;
51
+ }
52
+
53
+ plugins.push(ProjectConfigPluginEntry {
54
+ path: addon_path_as_devalang_protocol,
55
+ version: Some(addon.metadata.version.clone()),
56
+ });
57
+ }
58
+
59
+ // "preset" => {
60
+ // if config.presets.is_none() {
61
+ // config.presets = Some(Vec::new());
62
+ // }
63
+
64
+ // let presets = config.presets.as_mut().unwrap();
65
+
66
+ // let exists = presets.iter().any(|p| p.path == addon_path_as_deva_protocol);
67
+ // if exists {
68
+ // println!("Preset '{}' already in config", addon.name);
69
+ // continue;
70
+ // }
71
+
72
+ // presets.push(ProjectConfigPresetEntry {
73
+ // path: addon_path_as_deva_protocol,
74
+ // version: Some(addon.metadata.version.clone()),
75
+ // });
76
+ // }
77
+
78
+ // "template" => {
79
+ // if config.templates.is_none() {
80
+ // config.templates = Some(Vec::new());
81
+ // }
82
+
83
+ // let templates = config.templates.as_mut().unwrap();
84
+
85
+ // let exists = templates.iter().any(|t| t.path == addon_path_as_deva_protocol);
86
+ // if exists {
87
+ // println!("Template '{}' already in config", addon.name);
88
+ // continue;
89
+ // }
90
+
91
+ // templates.push(ProjectConfigTemplateEntry {
92
+ // path: addon_path_as_deva_protocol,
93
+ // version: Some(addon.metadata.version.clone()),
94
+ // });
95
+ // }
96
+ _ => {
97
+ println!(
98
+ "Unknown addon type '{}' for addon '{}'",
99
+ addon.addon_type, addon.name
100
+ );
101
+ }
102
+ }
103
+ }
104
+
105
+ // Update config with new addons
106
+ if let Err(e) = config.write_config(&config) {
107
+ return Err(format!("Failed to write config: {}", e));
108
+ }
109
+
110
+ Ok(())
111
+ }
@@ -0,0 +1,19 @@
1
+ use std::{fs, io, path::Path};
2
+
3
+ pub fn copy_dir_all(src: &Path, dst: &Path) -> Result<(), io::Error> {
4
+ if !dst.exists() {
5
+ fs::create_dir_all(dst)?;
6
+ }
7
+ for entry in fs::read_dir(src)? {
8
+ let entry = entry?;
9
+ let ty = entry.file_type()?;
10
+ let from = entry.path();
11
+ let to = dst.join(entry.file_name());
12
+ if ty.is_dir() {
13
+ copy_dir_all(&from, &to)?;
14
+ } else {
15
+ fs::copy(&from, &to)?;
16
+ }
17
+ }
18
+ Ok(())
19
+ }
@@ -0,0 +1,103 @@
1
+ use devalang_types::{AddonMetadata, AddonWithMetadata, DiscoveredAddon};
2
+ use devalang_utils::path as path_utils;
3
+
4
+ pub async fn install_selected_addons(
5
+ addons: Vec<DiscoveredAddon>,
6
+ ) -> Result<Vec<AddonWithMetadata>, String> {
7
+ let mut addons_enriched = Vec::new();
8
+
9
+ let tmp_dir = path_utils::ensure_deva_dir()?.join("tmp");
10
+
11
+ for addon in addons {
12
+ std::fs::create_dir_all(tmp_dir.join(&addon.name))
13
+ .map_err(|e| format!("Failed to create directory for addon {}: {}", addon.name, e))?;
14
+
15
+ let addon_path = tmp_dir.join(&addon.name);
16
+ devalang_utils::file::extract_zip_safely(&addon.path, &addon_path)
17
+ .map_err(|e| format!("Failed to extract addon {}: {}", addon.name, e))?;
18
+
19
+ let base = path_utils::ensure_deva_dir()?;
20
+ let target_addon_dir = match addon.addon_type.as_str() {
21
+ "bank" => base.join("banks"),
22
+ "plugin" => base.join("plugins"),
23
+ "preset" => base.join("presets"),
24
+ "template" => base.join("templates"),
25
+ _ => {
26
+ return Err(format!("Unknown addon type for addon {}", addon.name));
27
+ }
28
+ };
29
+
30
+ std::fs::create_dir_all(&target_addon_dir).map_err(|e| {
31
+ format!(
32
+ "Failed to create target directory for addon {}: {}",
33
+ addon.name, e
34
+ )
35
+ })?;
36
+
37
+ let target_addon_path_dir = target_addon_dir.join(&addon.name);
38
+ if target_addon_path_dir.exists() {
39
+ println!(
40
+ "Target addon directory {} already exists",
41
+ target_addon_path_dir.display()
42
+ );
43
+ continue;
44
+ }
45
+
46
+ if let Err(e) = std::fs::rename(&addon_path, &target_addon_path_dir) {
47
+ crate::cli::discover::fs::copy_dir_all(&addon_path, &target_addon_path_dir).map_err(
48
+ |err| {
49
+ format!(
50
+ "Failed to move addon {}: {} (rename error: {})",
51
+ addon.name, err, e
52
+ )
53
+ },
54
+ )?;
55
+ let _ = std::fs::remove_dir_all(&addon_path);
56
+ }
57
+
58
+ let addon_metadata_filename = match addon.addon_type.as_str() {
59
+ "bank" => "bank.toml",
60
+ "plugin" => "plugin.toml",
61
+ "preset" => "preset.toml",
62
+ "template" => "template.toml",
63
+ _ => {
64
+ return Err(format!("Unknown addon type for addon {}", addon.name));
65
+ }
66
+ };
67
+
68
+ let addon_metadata_path = target_addon_path_dir.join(addon_metadata_filename);
69
+ let addon_metadata_content = std::fs::read_to_string(&addon_metadata_path)
70
+ .map_err(|e| format!("Failed to read metadata for addon {}: {}", addon.name, e))?;
71
+
72
+ let parsed_meta = crate::cli::discover::metadata::parse_metadata_file(
73
+ &addon.addon_type,
74
+ &addon_metadata_content,
75
+ )
76
+ .unwrap_or(AddonMetadata {
77
+ name: addon.name.clone(),
78
+ author: "unknown".to_string(),
79
+ version: "".to_string(),
80
+ description: "".to_string(),
81
+ access: "".to_string(),
82
+ });
83
+
84
+ addons_enriched.push(AddonWithMetadata {
85
+ name: addon.name.clone(),
86
+ path: addon.path.clone().to_string_lossy().to_string(),
87
+ addon_type: addon.addon_type.clone(),
88
+ metadata: parsed_meta,
89
+ });
90
+
91
+ if let Err(e) = std::fs::remove_file(&addon.path) {
92
+ eprintln!(
93
+ "Failed to remove zipped file for addon {}: {}",
94
+ addon.name, e
95
+ );
96
+ }
97
+ }
98
+
99
+ // Best-effort cleanup of temporary extraction directory
100
+ let _ = std::fs::remove_dir_all(&tmp_dir);
101
+
102
+ Ok(addons_enriched)
103
+ }
@@ -0,0 +1,48 @@
1
+ use devalang_types::AddonMetadata;
2
+ use toml::Value;
3
+
4
+ pub fn parse_metadata_file(addon_type: &str, metadata_content: &str) -> Option<AddonMetadata> {
5
+ let parsed = metadata_content.parse::<Value>().ok()?;
6
+
7
+ let table = (match addon_type {
8
+ "bank" => parsed.get("bank"),
9
+ "plugin" => parsed.get("plugin"),
10
+ "preset" => parsed.get("preset"),
11
+ "template" => parsed.get("template"),
12
+ _ => None,
13
+ })?;
14
+
15
+ let name = table
16
+ .get("name")
17
+ .and_then(|v| v.as_str())
18
+ .unwrap_or("")
19
+ .to_string();
20
+ let version = table
21
+ .get("version")
22
+ .and_then(|v| v.as_str())
23
+ .unwrap_or("")
24
+ .to_string();
25
+ let description = table
26
+ .get("description")
27
+ .and_then(|v| v.as_str())
28
+ .unwrap_or("")
29
+ .to_string();
30
+ let author = table
31
+ .get("author")
32
+ .and_then(|v| v.as_str())
33
+ .unwrap_or("unknown")
34
+ .to_string();
35
+ let access = table
36
+ .get("access")
37
+ .and_then(|v| v.as_str())
38
+ .unwrap_or("")
39
+ .to_string();
40
+
41
+ Some(AddonMetadata {
42
+ name,
43
+ author,
44
+ version,
45
+ description,
46
+ access,
47
+ })
48
+ }
@@ -0,0 +1,5 @@
1
+ pub mod commands;
2
+ pub mod config;
3
+ pub mod fs;
4
+ pub mod install;
5
+ pub mod metadata;