@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,111 +1,109 @@
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
- }
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.publisher, 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
+ });
35
+ }
36
+
37
+ "plugin" => {
38
+ if config.plugins.is_none() {
39
+ config.plugins = Some(Vec::new());
40
+ }
41
+
42
+ let plugins = config.plugins.as_mut().unwrap();
43
+
44
+ let exists = plugins
45
+ .iter()
46
+ .any(|p| p.path == addon_path_as_devalang_protocol);
47
+ if exists {
48
+ println!("Plugin '{}' already in config", addon.name);
49
+ continue;
50
+ }
51
+
52
+ plugins.push(ProjectConfigPluginEntry {
53
+ path: addon_path_as_devalang_protocol,
54
+ });
55
+ }
56
+
57
+ // "preset" => {
58
+ // if config.presets.is_none() {
59
+ // config.presets = Some(Vec::new());
60
+ // }
61
+
62
+ // let presets = config.presets.as_mut().unwrap();
63
+
64
+ // let exists = presets.iter().any(|p| p.path == addon_path_as_deva_protocol);
65
+ // if exists {
66
+ // println!("Preset '{}' already in config", addon.name);
67
+ // continue;
68
+ // }
69
+
70
+ // presets.push(ProjectConfigPresetEntry {
71
+ // path: addon_path_as_deva_protocol,
72
+ // version: Some(addon.metadata.version.clone()),
73
+ // });
74
+ // }
75
+
76
+ // "template" => {
77
+ // if config.templates.is_none() {
78
+ // config.templates = Some(Vec::new());
79
+ // }
80
+
81
+ // let templates = config.templates.as_mut().unwrap();
82
+
83
+ // let exists = templates.iter().any(|t| t.path == addon_path_as_deva_protocol);
84
+ // if exists {
85
+ // println!("Template '{}' already in config", addon.name);
86
+ // continue;
87
+ // }
88
+
89
+ // templates.push(ProjectConfigTemplateEntry {
90
+ // path: addon_path_as_deva_protocol,
91
+ // version: Some(addon.metadata.version.clone()),
92
+ // });
93
+ // }
94
+ _ => {
95
+ println!(
96
+ "Unknown addon type '{}' for addon '{}'",
97
+ addon.addon_type, addon.name
98
+ );
99
+ }
100
+ }
101
+ }
102
+
103
+ // Update config with new addons
104
+ if let Err(e) = config.write_config(&config) {
105
+ return Err(format!("Failed to write config: {}", e));
106
+ }
107
+
108
+ Ok(())
109
+ }
@@ -1,19 +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
- }
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
+ }
@@ -1,103 +1,214 @@
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
- }
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
+ no_clear_tmp: bool,
7
+ ) -> Result<Vec<AddonWithMetadata>, String> {
8
+ let mut addons_enriched = Vec::new();
9
+
10
+ let tmp_dir = path_utils::ensure_deva_dir()?.join("tmp");
11
+
12
+ for addon in addons {
13
+ std::fs::create_dir_all(tmp_dir.join(&addon.name))
14
+ .map_err(|e| format!("Failed to create directory for addon {}: {}", addon.name, e))?;
15
+
16
+ let addon_path = tmp_dir.join(&addon.name);
17
+ devalang_utils::file::extract_zip_safely(&addon.path, &addon_path)
18
+ .map_err(|e| format!("Failed to extract addon {}: {}", addon.name, e))?;
19
+
20
+ // Read metadata from the extracted addon first so we can layout as <publisher>/<name>
21
+ // If the addon type is unknown (we found a .tar.gz), try to detect the
22
+ // metadata file inside the extracted folder to discover the real type.
23
+ let mut detected_addon_type = addon.addon_type.clone();
24
+ let mut parsed_meta_tmp = None;
25
+
26
+ if detected_addon_type == "unknown" {
27
+ // try detection by checking common metadata filenames
28
+ let candidates = ["bank.toml", "plugin.toml", "preset.toml", "template.toml"];
29
+ for candidate in &candidates {
30
+ let path = addon_path.join(candidate);
31
+ if path.exists() {
32
+ detected_addon_type = match *candidate {
33
+ "bank.toml" => "bank".to_string(),
34
+ "plugin.toml" => "plugin".to_string(),
35
+ "preset.toml" => "preset".to_string(),
36
+ "template.toml" => "template".to_string(),
37
+ _ => "unknown".to_string(),
38
+ };
39
+ if let Ok(content) = std::fs::read_to_string(&path) {
40
+ parsed_meta_tmp = crate::cli::discover::metadata::parse_metadata_file(
41
+ &detected_addon_type,
42
+ &content,
43
+ );
44
+ }
45
+ break;
46
+ }
47
+ }
48
+ } else {
49
+ let addon_metadata_filename = match addon.addon_type.as_str() {
50
+ "bank" => "bank.toml",
51
+ "plugin" => "plugin.toml",
52
+ "preset" => "preset.toml",
53
+ "template" => "template.toml",
54
+ _ => "",
55
+ };
56
+ if !addon_metadata_filename.is_empty() {
57
+ let addon_metadata_path_tmp = addon_path.join(addon_metadata_filename);
58
+ let addon_metadata_content_tmp =
59
+ std::fs::read_to_string(&addon_metadata_path_tmp).ok();
60
+ if let Some(content) = addon_metadata_content_tmp {
61
+ parsed_meta_tmp = crate::cli::discover::metadata::parse_metadata_file(
62
+ &addon.addon_type,
63
+ &content,
64
+ );
65
+ }
66
+ }
67
+ }
68
+
69
+ let base = path_utils::ensure_deva_dir()?;
70
+ // Use detected_addon_type (may have been found from metadata)
71
+ // final_addon_type is what we will report and use for metadata lookup
72
+ let final_addon_type = if detected_addon_type == "unknown" {
73
+ addon.addon_type.clone()
74
+ } else {
75
+ detected_addon_type.clone()
76
+ };
77
+
78
+ let target_addon_dir = match final_addon_type.as_str() {
79
+ "bank" => base.join("banks"),
80
+ "plugin" => base.join("plugins"),
81
+ "preset" => base.join("presets"),
82
+ "template" => base.join("templates"),
83
+ _ => {
84
+ // Fallback: place unknown archives into plugins by default
85
+ base.join("plugins")
86
+ }
87
+ };
88
+
89
+ std::fs::create_dir_all(&target_addon_dir).map_err(|e| {
90
+ format!(
91
+ "Failed to create target directory for addon {}: {}",
92
+ addon.name, e
93
+ )
94
+ })?;
95
+
96
+ // prefer parsed metadata publisher/name, fall back to discovered publisher/name
97
+ let publisher_folder = parsed_meta_tmp
98
+ .as_ref()
99
+ .and_then(|m| {
100
+ if !m.publisher.is_empty() {
101
+ Some(m.publisher.clone())
102
+ } else {
103
+ None
104
+ }
105
+ })
106
+ .unwrap_or_else(|| addon.publisher.clone());
107
+
108
+ let name_folder = parsed_meta_tmp
109
+ .as_ref()
110
+ .and_then(|m| {
111
+ if !m.name.is_empty() {
112
+ Some(m.name.clone())
113
+ } else {
114
+ None
115
+ }
116
+ })
117
+ .unwrap_or_else(|| addon.name.clone());
118
+
119
+ let target_addon_path_dir = target_addon_dir.join(&publisher_folder).join(&name_folder);
120
+ if target_addon_path_dir.exists() {
121
+ println!(
122
+ "Target addon directory {} already exists",
123
+ target_addon_path_dir.display()
124
+ );
125
+ continue;
126
+ }
127
+
128
+ if let Err(e) = std::fs::rename(&addon_path, &target_addon_path_dir) {
129
+ crate::cli::discover::fs::copy_dir_all(&addon_path, &target_addon_path_dir).map_err(
130
+ |err| {
131
+ format!(
132
+ "Failed to move addon {}: {} (rename error: {})",
133
+ addon.name, err, e
134
+ )
135
+ },
136
+ )?;
137
+ let _ = std::fs::remove_dir_all(&addon_path);
138
+ }
139
+ // Attempt to read final metadata file (use parsed_tmp as fallback)
140
+ let addon_metadata_filename = match final_addon_type.as_str() {
141
+ "bank" => "bank.toml",
142
+ "plugin" => "plugin.toml",
143
+ "preset" => "preset.toml",
144
+ "template" => "template.toml",
145
+ _ => "",
146
+ };
147
+
148
+ let parsed_meta = if addon_metadata_filename.is_empty() {
149
+ parsed_meta_tmp.unwrap_or(AddonMetadata {
150
+ name: "".to_string(),
151
+ publisher: "".to_string(),
152
+ version: "".to_string(),
153
+ description: "".to_string(),
154
+ access: "".to_string(),
155
+ })
156
+ } else {
157
+ let addon_metadata_path = target_addon_path_dir.join(addon_metadata_filename);
158
+ if let Ok(content) = std::fs::read_to_string(&addon_metadata_path) {
159
+ crate::cli::discover::metadata::parse_metadata_file(&final_addon_type, &content)
160
+ .unwrap_or(parsed_meta_tmp.unwrap_or(AddonMetadata {
161
+ name: "".to_string(),
162
+ publisher: "".to_string(),
163
+ version: "".to_string(),
164
+ description: "".to_string(),
165
+ access: "".to_string(),
166
+ }))
167
+ } else {
168
+ parsed_meta_tmp.unwrap_or(AddonMetadata {
169
+ name: "".to_string(),
170
+ publisher: "".to_string(),
171
+ version: "".to_string(),
172
+ description: "".to_string(),
173
+ access: "".to_string(),
174
+ })
175
+ }
176
+ };
177
+
178
+ // Record location as the final moved directory path
179
+ addons_enriched.push(AddonWithMetadata {
180
+ name: name_folder.clone(),
181
+ publisher: publisher_folder.clone(),
182
+ path: target_addon_path_dir.clone().to_string_lossy().to_string(),
183
+ addon_type: final_addon_type.clone(),
184
+ metadata: parsed_meta,
185
+ });
186
+
187
+ if let Err(e) = std::fs::remove_file(&addon.path) {
188
+ eprintln!(
189
+ "Failed to remove zipped file for addon {}: {}",
190
+ addon.name, e
191
+ );
192
+ }
193
+ }
194
+
195
+ // Best-effort cleanup of temporary extraction directory: remove `.deva/tmp`
196
+ // only if it's empty after installing the selected addons and when the
197
+ // user did not pass --no-clear-tmp.
198
+ if !no_clear_tmp {
199
+ if tmp_dir.exists() {
200
+ match std::fs::read_dir(&tmp_dir) {
201
+ Ok(mut rd) => {
202
+ if rd.next().is_none() {
203
+ let _ = std::fs::remove_dir_all(&tmp_dir);
204
+ }
205
+ }
206
+ Err(_) => {
207
+ // ignore errors here - best-effort
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ Ok(addons_enriched)
214
+ }