@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
@@ -0,0 +1,271 @@
1
+ use crate::config::ops::load_config;
2
+ use devalang_core::config::driver::ProjectConfigExt;
3
+ use devalang_utils::path as path_utils;
4
+ use std::fs;
5
+
6
+ pub async fn remove_addon(name: String) -> Result<(), String> {
7
+ let deva_dir = path_utils::ensure_deva_dir()?;
8
+
9
+ // Helper to extract publisher from a slug like 'publisher.name'
10
+ let extract_publisher = |s: &str| s.splitn(2, '.').next().unwrap_or("").to_string();
11
+
12
+ // Try to find in config first (banks/plugins)
13
+ if let Ok(config_path) = path_utils::get_devalang_config_path() {
14
+ if let Some(mut config) = load_config(Some(&config_path)) {
15
+ // BANKS
16
+ if let Some(banks) = config.banks.as_mut() {
17
+ if let Some(pos) = banks.iter().position(|b| {
18
+ let slug = b.path.strip_prefix("devalang://bank/").unwrap_or(&b.path);
19
+ // accept exact slug, exact path, or match by local name suffix (publisher/name or publisher.name)
20
+ slug == name
21
+ || b.path == name
22
+ || slug.ends_with(&format!("/{}", name))
23
+ || slug.ends_with(&format!(".{}", name))
24
+ }) {
25
+ let entry = banks.remove(pos);
26
+ let slug = entry
27
+ .path
28
+ .strip_prefix("devalang://bank/")
29
+ .unwrap_or(&entry.path)
30
+ .to_string();
31
+
32
+ // parse publisher/name from slug (support 'publisher/name' or 'publisher.name')
33
+ let (publisher, local_name) = if slug.contains('/') {
34
+ let mut it = slug.splitn(2, '/');
35
+ (
36
+ it.next().unwrap().to_string(),
37
+ it.next().unwrap().to_string(),
38
+ )
39
+ } else if slug.contains('.') {
40
+ let mut it = slug.splitn(2, '.');
41
+ (
42
+ it.next().unwrap().to_string(),
43
+ it.next().unwrap().to_string(),
44
+ )
45
+ } else {
46
+ return Err(format!("Cannot parse bank slug '{}'", slug));
47
+ };
48
+
49
+ let local_path = deva_dir.join("banks").join(&publisher).join(&local_name);
50
+
51
+ if !local_path.exists() {
52
+ return Err(format!(
53
+ "Local files for bank '{}' not found at '{}', aborting",
54
+ slug,
55
+ local_path.display()
56
+ ));
57
+ }
58
+
59
+ fs::remove_dir_all(&local_path)
60
+ .map_err(|e| format!("Failed to remove addon files: {}", e))?;
61
+
62
+ if let Err(e) = config.write_config(&config) {
63
+ eprintln!("Warning: failed to write updated config: {}", e);
64
+ }
65
+
66
+ println!("✅ Bank '{}' removed (publisher '{}')", slug, publisher);
67
+ return Ok(());
68
+ }
69
+ }
70
+
71
+ // PLUGINS
72
+ if let Some(plugins) = config.plugins.as_mut() {
73
+ if let Some(pos) = plugins.iter().position(|p| {
74
+ let slug = p.path.strip_prefix("devalang://plugin/").unwrap_or(&p.path);
75
+ // accept exact slug, exact path, or match by local name suffix
76
+ slug == name
77
+ || p.path == name
78
+ || slug.ends_with(&format!("/{}", name))
79
+ || slug.ends_with(&format!(".{}", name))
80
+ }) {
81
+ let entry = plugins.remove(pos);
82
+ let slug = entry
83
+ .path
84
+ .strip_prefix("devalang://plugin/")
85
+ .unwrap_or(&entry.path)
86
+ .to_string();
87
+
88
+ // parse publisher/name from slug (support 'publisher/name' or 'publisher.name')
89
+ let (publisher, local_name) = if slug.contains('/') {
90
+ let mut it = slug.splitn(2, '/');
91
+ (
92
+ it.next().unwrap().to_string(),
93
+ it.next().unwrap().to_string(),
94
+ )
95
+ } else if slug.contains('.') {
96
+ let mut it = slug.splitn(2, '.');
97
+ (
98
+ it.next().unwrap().to_string(),
99
+ it.next().unwrap().to_string(),
100
+ )
101
+ } else {
102
+ return Err(format!("Cannot parse plugin slug '{}'", slug));
103
+ };
104
+
105
+ let local_path = deva_dir.join("plugins").join(&publisher).join(&local_name);
106
+
107
+ if !local_path.exists() {
108
+ return Err(format!(
109
+ "Local files for plugin '{}' not found at '{}', aborting",
110
+ slug,
111
+ local_path.display()
112
+ ));
113
+ }
114
+
115
+ fs::remove_dir_all(&local_path)
116
+ .map_err(|e| format!("Failed to remove addon files: {}", e))?;
117
+
118
+ if let Err(e) = config.write_config(&config) {
119
+ eprintln!("Warning: failed to write updated config: {}", e);
120
+ }
121
+
122
+ println!("✅ Plugin '{}' removed (publisher '{}')", slug, publisher);
123
+ return Ok(());
124
+ }
125
+ }
126
+ }
127
+ }
128
+
129
+ // Not found in config: search filesystem under .deva and infer type
130
+ let dirs = ["banks", "plugins", "presets", "templates"];
131
+ for &d in &dirs {
132
+ let folder = deva_dir.join(d);
133
+ if !folder.exists() {
134
+ continue;
135
+ }
136
+
137
+ // If name looks like a slug (contains '.' or '/'), parse publisher and name and try candidate paths
138
+ if name.contains('.') || name.contains('/') {
139
+ let (publisher, local_name) = if name.contains('/') {
140
+ let mut it = name.splitn(2, '/');
141
+ (
142
+ it.next().unwrap().to_string(),
143
+ it.next().unwrap().to_string(),
144
+ )
145
+ } else {
146
+ let mut it = name.splitn(2, '.');
147
+ (
148
+ it.next().unwrap().to_string(),
149
+ it.next().unwrap().to_string(),
150
+ )
151
+ };
152
+
153
+ let candidate1 = folder.join(&publisher).join(&local_name);
154
+ let candidate2 = folder.join(format!("{}.{}", publisher, local_name));
155
+ let candidate3 = folder.join(&name);
156
+
157
+ let candidate = if candidate1.exists() {
158
+ candidate1
159
+ } else if candidate2.exists() {
160
+ candidate2
161
+ } else {
162
+ candidate3
163
+ };
164
+
165
+ if candidate.exists() {
166
+ fs::remove_dir_all(&candidate)
167
+ .map_err(|e| format!("Failed to remove addon files: {}", e))?;
168
+
169
+ // also attempt to remove from config if possible
170
+ if let Ok(config_path) = path_utils::get_devalang_config_path() {
171
+ if let Some(mut config) = load_config(Some(&config_path)) {
172
+ match d {
173
+ "banks" => {
174
+ if let Some(banks) = config.banks.as_mut() {
175
+ let pattern1 =
176
+ format!("devalang://bank/{}/{}", publisher, local_name);
177
+ let pattern2 =
178
+ format!("devalang://bank/{}.{}", publisher, local_name);
179
+ banks.retain(|b| {
180
+ b.path != pattern1
181
+ && b.path != pattern2
182
+ && !b.path.ends_with(&local_name)
183
+ });
184
+ }
185
+ }
186
+ "plugins" => {
187
+ if let Some(plugins) = config.plugins.as_mut() {
188
+ let pattern1 =
189
+ format!("devalang://plugin/{}/{}", publisher, local_name);
190
+ let pattern2 =
191
+ format!("devalang://plugin/{}.{}", publisher, local_name);
192
+ plugins.retain(|p| {
193
+ p.path != pattern1
194
+ && p.path != pattern2
195
+ && !p.path.ends_with(&local_name)
196
+ });
197
+ }
198
+ }
199
+ _ => {}
200
+ }
201
+
202
+ if let Err(e) = config.write_config(&config) {
203
+ eprintln!("Warning: failed to write updated config: {}", e);
204
+ }
205
+ }
206
+ }
207
+
208
+ println!(
209
+ "✅ Addon '{}/{}' removed from .deva/{} (publisher '{}')",
210
+ publisher, local_name, d, publisher
211
+ );
212
+ return Ok(());
213
+ }
214
+ }
215
+
216
+ // Otherwise, scan directory entries: match exact name or suffix '.name'
217
+ if let Ok(entries) = fs::read_dir(&folder) {
218
+ for entry in entries.flatten() {
219
+ if let Ok(file_type) = entry.file_type() {
220
+ if !file_type.is_dir() {
221
+ continue;
222
+ }
223
+ }
224
+ let file_name = entry.file_name();
225
+ let file_name = file_name.to_string_lossy();
226
+ if file_name == name || file_name.ends_with(&format!(".{}", name)) {
227
+ let slug = file_name.to_string();
228
+ let publisher = extract_publisher(&slug);
229
+ let path = entry.path();
230
+ fs::remove_dir_all(&path)
231
+ .map_err(|e| format!("Failed to remove addon files: {}", e))?;
232
+
233
+ // try to remove from config when banks/plugins
234
+ if let Ok(config_path) = path_utils::get_devalang_config_path() {
235
+ if let Some(mut config) = load_config(Some(&config_path)) {
236
+ match d {
237
+ "banks" => {
238
+ if let Some(banks) = config.banks.as_mut() {
239
+ banks.retain(|b| {
240
+ b.path != format!("devalang://bank/{}", slug)
241
+ });
242
+ }
243
+ }
244
+ "plugins" => {
245
+ if let Some(plugins) = config.plugins.as_mut() {
246
+ plugins.retain(|p| {
247
+ p.path != format!("devalang://plugin/{}", slug)
248
+ });
249
+ }
250
+ }
251
+ _ => {}
252
+ }
253
+
254
+ if let Err(e) = config.write_config(&config) {
255
+ eprintln!("Warning: failed to write updated config: {}", e);
256
+ }
257
+ }
258
+ }
259
+
260
+ println!(
261
+ "✅ Addon '{}' removed from .deva/{} (publisher '{}')",
262
+ slug, d, publisher
263
+ );
264
+ return Ok(());
265
+ }
266
+ }
267
+ }
268
+ }
269
+
270
+ Err(format!("Addon '{}' not found", name))
271
+ }
@@ -0,0 +1,305 @@
1
+ use crate::config::ops::load_config;
2
+ use crate::{
3
+ cli::addon::{
4
+ download::download_addon,
5
+ metadata::{get_addon_from_api, get_addon_publisher_from_api},
6
+ },
7
+ web::cdn::get_cdn_url,
8
+ };
9
+ use devalang_core::config::driver::ProjectConfigExt;
10
+ use devalang_types::AddonType;
11
+ use devalang_utils::path as path_utils;
12
+ use std::fs;
13
+ use toml::Value as TomlValue;
14
+
15
+ #[derive(serde::Deserialize)]
16
+ pub struct AddonVersion {
17
+ pub version: String,
18
+ }
19
+
20
+ async fn fetch_latest_version(
21
+ addon_type: AddonType,
22
+ addon_name: String,
23
+ ) -> Result<AddonVersion, Box<dyn std::error::Error>> {
24
+ let cdn_url = get_cdn_url();
25
+
26
+ let addon_type = match addon_type {
27
+ AddonType::Bank => "bank",
28
+ AddonType::Plugin => "plugin",
29
+ AddonType::Preset => "preset",
30
+ AddonType::Template => "template",
31
+ };
32
+
33
+ let publisher_identifier = get_addon_publisher_from_api(&addon_name)
34
+ .await
35
+ .unwrap_or("unknown".to_string());
36
+
37
+ let url = format!(
38
+ "{}/{}/{}/{}/version",
39
+ cdn_url, addon_type, publisher_identifier, addon_name
40
+ );
41
+
42
+ let response = reqwest::get(url).await?;
43
+
44
+ if !response.status().is_success() {
45
+ return Err(format!("❌ Failed to fetch version: HTTP {}", response.status()).into());
46
+ }
47
+
48
+ let bytes = response.bytes().await?;
49
+
50
+ let version: AddonVersion = serde_json::from_slice(&bytes)?;
51
+
52
+ Ok(version)
53
+ }
54
+
55
+ pub async fn update_addon(slug: String) -> Result<(), String> {
56
+ let addon_metadata = get_addon_from_api(&slug).await?;
57
+ let deva_dir = path_utils::ensure_deva_dir()?;
58
+
59
+ // Represent the addon as "<publisher>.<addon>" for user-visible names
60
+ let publisher_and_name = format!("{}.{}", addon_metadata.publisher, addon_metadata.name);
61
+
62
+ match addon_metadata.addon_type {
63
+ devalang_types::AddonType::Bank => {
64
+ match fetch_latest_version(
65
+ addon_metadata.addon_type.clone(),
66
+ addon_metadata.name.clone(),
67
+ )
68
+ .await
69
+ {
70
+ Ok(latest) => {
71
+ // Determine local version from bank.toml if available
72
+ let local_bank_path = deva_dir
73
+ .join("banks")
74
+ .join(&addon_metadata.publisher)
75
+ .join(&addon_metadata.name);
76
+ let local_version = if local_bank_path.exists() {
77
+ let bank_toml = local_bank_path.join("bank.toml");
78
+ if bank_toml.exists() {
79
+ if let Ok(content) = fs::read_to_string(&bank_toml) {
80
+ if let Ok(parsed) =
81
+ toml::from_str::<devalang_types::BankFile>(&content)
82
+ {
83
+ parsed.bank.version
84
+ } else {
85
+ String::new()
86
+ }
87
+ } else {
88
+ String::new()
89
+ }
90
+ } else {
91
+ String::new()
92
+ }
93
+ } else {
94
+ String::new()
95
+ };
96
+
97
+ if local_version != latest.version {
98
+ println!(
99
+ "Updating bank '{}' from '{}' to '{}'...",
100
+ publisher_and_name, local_version, latest.version
101
+ );
102
+
103
+ // remove existing folder if present
104
+ let bank_dir = deva_dir
105
+ .join("banks")
106
+ .join(&addon_metadata.publisher)
107
+ .join(&addon_metadata.name);
108
+ if bank_dir.exists() {
109
+ fs::remove_dir_all(&bank_dir)
110
+ .map_err(|e| format!("Failed to remove old bank files: {}", e))?;
111
+ }
112
+
113
+ // download new (use publisher.addon as the external identifier)
114
+ download_addon(&publisher_and_name, &addon_metadata).await?;
115
+
116
+ // update config version when present
117
+ if let Ok(config_path) = path_utils::get_devalang_config_path() {
118
+ if let Some(mut config) = load_config(Some(&config_path)) {
119
+ if let Some(banks) = config.banks.as_mut() {
120
+ for bank in banks.iter_mut() {
121
+ let name_in_path = bank
122
+ .path
123
+ .strip_prefix("devalang://bank/")
124
+ .unwrap_or(&bank.path);
125
+ }
126
+
127
+ if let Err(e) = config.write_config(&config) {
128
+ eprintln!("Warning: failed to write updated config: {}", e);
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ println!(
135
+ "✅ Bank '{}' updated to version '{}'",
136
+ publisher_and_name, latest.version
137
+ );
138
+ } else {
139
+ println!(
140
+ "Bank '{}' is already up-to-date (version {})",
141
+ publisher_and_name, latest.version
142
+ );
143
+ }
144
+ }
145
+ Err(e) => {
146
+ return Err(format!(
147
+ "Failed to fetch latest version for bank '{}': {}",
148
+ publisher_and_name, e
149
+ ));
150
+ }
151
+ }
152
+ }
153
+
154
+ devalang_types::AddonType::Plugin => {
155
+ // Try to fetch latest version via CDN API; if available compare, otherwise fallback to redownload
156
+ match fetch_latest_version(
157
+ addon_metadata.addon_type.clone(),
158
+ addon_metadata.name.clone(),
159
+ )
160
+ .await
161
+ {
162
+ Ok(latest) => {
163
+ // Determine local plugin version by reading plugin.toml from preferred or fallback layout
164
+ let preferred = deva_dir.join("plugins").join(format!(
165
+ "{}/{}",
166
+ addon_metadata.publisher, addon_metadata.name
167
+ ));
168
+ let fallback = deva_dir
169
+ .join("plugins")
170
+ .join(&addon_metadata.publisher)
171
+ .join(&addon_metadata.name);
172
+
173
+ let mut local_version = String::new();
174
+ for candidate in [&preferred, &fallback] {
175
+ let toml_path = candidate.join("plugin.toml");
176
+ if toml_path.exists() {
177
+ if let Ok(content) = fs::read_to_string(&toml_path) {
178
+ if let Ok(value) = toml::from_str::<TomlValue>(&content) {
179
+ if let Some(v) = value
180
+ .get("plugin")
181
+ .and_then(|p| p.get("version"))
182
+ .and_then(|s| s.as_str())
183
+ {
184
+ local_version = v.to_string();
185
+ }
186
+ }
187
+ }
188
+ break;
189
+ }
190
+ }
191
+
192
+ if local_version != latest.version {
193
+ println!(
194
+ "Updating plugin '{}' from '{}' to '{}'...",
195
+ publisher_and_name, local_version, latest.version
196
+ );
197
+
198
+ // remove any existing layout
199
+ if preferred.exists() {
200
+ fs::remove_dir_all(&preferred)
201
+ .map_err(|e| format!("Failed to remove old plugin files: {}", e))?;
202
+ }
203
+ if fallback.exists() {
204
+ fs::remove_dir_all(&fallback)
205
+ .map_err(|e| format!("Failed to remove old plugin files: {}", e))?;
206
+ }
207
+
208
+ // download new
209
+ download_addon(&publisher_and_name, &addon_metadata).await?;
210
+
211
+ // update config version when present
212
+ if let Ok(config_path) = path_utils::get_devalang_config_path() {
213
+ if let Some(mut config) = load_config(Some(&config_path)) {
214
+ if let Some(plugins) = config.plugins.as_mut() {
215
+ for p in plugins.iter_mut() {
216
+ let name_in_path = p
217
+ .path
218
+ .strip_prefix("devalang://plugin/")
219
+ .unwrap_or(&p.path);
220
+ }
221
+
222
+ if let Err(e) = config.write_config(&config) {
223
+ eprintln!("Warning: failed to write updated config: {}", e);
224
+ }
225
+ }
226
+ }
227
+ }
228
+
229
+ println!(
230
+ "✅ Plugin '{}' updated to version '{}'",
231
+ publisher_and_name, latest.version
232
+ );
233
+ } else {
234
+ println!(
235
+ "Plugin '{}' is already up-to-date (version {})",
236
+ publisher_and_name, latest.version
237
+ );
238
+ }
239
+ }
240
+ Err(_) => {
241
+ // Fallback: redownload everything
242
+ println!(
243
+ "No version info for plugin '{}', redownloading to ensure latest.",
244
+ publisher_and_name
245
+ );
246
+
247
+ let plugin_dir = deva_dir
248
+ .join("plugins")
249
+ .join(&addon_metadata.publisher)
250
+ .join(&addon_metadata.name);
251
+ if plugin_dir.exists() {
252
+ fs::remove_dir_all(&plugin_dir)
253
+ .map_err(|e| format!("Failed to remove old plugin files: {}", e))?;
254
+ }
255
+
256
+ download_addon(&publisher_and_name, &addon_metadata).await?;
257
+
258
+ // clear version in config (unknown)
259
+ if let Ok(config_path) = path_utils::get_devalang_config_path() {
260
+ if let Some(mut config) = load_config(Some(&config_path)) {
261
+ if let Some(plugins) = config.plugins.as_mut() {
262
+ for p in plugins.iter_mut() {
263
+ let name_in_path = p
264
+ .path
265
+ .strip_prefix("devalang://plugin/")
266
+ .unwrap_or(&p.path);
267
+ }
268
+
269
+ if let Err(e) = config.write_config(&config) {
270
+ eprintln!("Warning: failed to write updated config: {}", e);
271
+ }
272
+ }
273
+ }
274
+ }
275
+
276
+ println!("✅ Plugin '{}' updated", publisher_and_name);
277
+ }
278
+ }
279
+ }
280
+
281
+ devalang_types::AddonType::Preset | devalang_types::AddonType::Template => {
282
+ println!(
283
+ "Update for presets/templates is not yet implemented; reinstalling to be safe."
284
+ );
285
+ let target_dir = match addon_metadata.addon_type {
286
+ devalang_types::AddonType::Preset => deva_dir.join("presets"),
287
+ _ => deva_dir.join("templates"),
288
+ };
289
+
290
+ let candidate = target_dir
291
+ .join(&addon_metadata.publisher)
292
+ .join(&addon_metadata.name);
293
+ if candidate.exists() {
294
+ fs::remove_dir_all(&candidate)
295
+ .map_err(|e| format!("Failed to remove old files: {}", e))?;
296
+ }
297
+
298
+ // use publisher.addon representation for user-facing messaging
299
+ download_addon(&publisher_and_name, &addon_metadata).await?;
300
+ println!("✅ Addon '{}' updated (reinstalled)", publisher_and_name);
301
+ }
302
+ }
303
+
304
+ Ok(())
305
+ }