@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.
- package/Cargo.toml +84 -81
- package/README.md +3 -2
- package/docs/CHANGELOG.md +41 -0
- package/docs/ROADMAP.md +3 -3
- package/examples/chain.deva +19 -0
- package/examples/plugin.deva +10 -10
- package/examples/routing.deva +23 -0
- package/out-tsc/bin/project-version.json +6 -0
- package/out-tsc/pkg/devalang_core_bg.wasm.d.ts +8 -8
- package/out-tsc/scripts/version/copy-to-binary.d.ts +1 -0
- package/out-tsc/scripts/version/copy-to-binary.js +79 -0
- package/package.json +23 -10
- package/project-version.json +3 -3
- package/rust/bindings/Cargo.toml +9 -0
- package/rust/bindings/src/lib.rs +86 -0
- package/rust/cli/addon/commands.rs +35 -0
- package/rust/cli/addon/download.rs +234 -0
- package/rust/cli/addon/install.rs +33 -0
- package/rust/cli/addon/list.rs +224 -0
- package/rust/cli/addon/metadata.rs +124 -0
- package/rust/cli/addon/mod.rs +8 -0
- package/rust/cli/addon/remove.rs +271 -0
- package/rust/cli/addon/update.rs +305 -0
- package/rust/cli/{install/addon.rs → addon/utils.rs} +109 -118
- package/rust/cli/build/commands.rs +153 -153
- package/rust/cli/build/process.rs +165 -165
- package/rust/cli/check/mod.rs +208 -208
- package/rust/cli/discover/commands.rs +275 -253
- package/rust/cli/discover/config.rs +109 -111
- package/rust/cli/discover/fs.rs +19 -19
- package/rust/cli/discover/install.rs +214 -103
- package/rust/cli/discover/metadata.rs +48 -48
- package/rust/cli/discover/mod.rs +5 -5
- package/rust/cli/me/commands.rs +52 -0
- package/rust/cli/me/mod.rs +1 -0
- package/rust/cli/mod.rs +12 -12
- package/rust/cli/parser.rs +30 -69
- package/rust/cli/play/commands.rs +375 -375
- package/rust/cli/play/process.rs +159 -159
- package/rust/core/audio/engine/driver.rs +19 -2
- package/rust/core/audio/engine/export.rs +169 -169
- package/rust/core/audio/engine/mod.rs +56 -56
- package/rust/core/audio/engine/notes/dsp.rs +88 -85
- package/rust/core/audio/engine/notes/mod.rs +53 -44
- package/rust/core/audio/engine/notes/params.rs +294 -294
- package/rust/core/audio/engine/sample/insert.rs +148 -47
- package/rust/core/audio/engine/sample/mod.rs +40 -40
- package/rust/core/audio/engine/sample/padding.rs +170 -170
- package/rust/core/audio/evaluator/condition.rs +61 -61
- package/rust/core/audio/evaluator/numeric.rs +152 -152
- package/rust/core/audio/evaluator/rhs.rs +16 -16
- package/rust/core/audio/evaluator/string_expr.rs +94 -94
- package/rust/core/audio/interpreter/driver.rs +574 -574
- package/rust/core/audio/interpreter/mod.rs +2 -2
- package/rust/core/audio/interpreter/statements/arrow_call/interprete.rs +9 -5
- package/rust/core/audio/interpreter/statements/arrow_call/methods/chord.rs +398 -384
- package/rust/core/audio/interpreter/statements/arrow_call/methods/effects.rs +323 -0
- package/rust/core/audio/interpreter/statements/arrow_call/methods/mod.rs +1 -0
- package/rust/core/audio/interpreter/statements/arrow_call/methods/note.rs +66 -11
- package/rust/core/audio/interpreter/statements/arrow_call/mod.rs +3 -3
- package/rust/core/audio/interpreter/statements/arrow_call/types/arp.rs +192 -192
- package/rust/core/audio/interpreter/statements/arrow_call/types/mod.rs +24 -24
- package/rust/core/audio/interpreter/statements/arrow_call/types/pad.rs +116 -116
- package/rust/core/audio/interpreter/statements/arrow_call/types/pluck.rs +97 -97
- package/rust/core/audio/interpreter/statements/arrow_call/types/sub.rs +100 -100
- package/rust/core/audio/interpreter/statements/automate.rs +16 -16
- package/rust/core/audio/interpreter/statements/call.rs +31 -1
- package/rust/core/audio/interpreter/statements/condition.rs +72 -72
- package/rust/core/audio/interpreter/statements/function.rs +24 -24
- package/rust/core/audio/interpreter/statements/let_.rs +36 -36
- package/rust/core/audio/interpreter/statements/load.rs +17 -17
- package/rust/core/audio/interpreter/statements/loop_.rs +115 -115
- package/rust/core/audio/interpreter/statements/spawn.rs +51 -2
- package/rust/core/audio/interpreter/statements/trigger.rs +242 -239
- package/rust/core/audio/loader/trigger.rs +98 -98
- package/rust/core/audio/player.rs +70 -70
- package/rust/core/audio/special/mod.rs +9 -9
- package/rust/core/builder/mod.rs +129 -129
- package/rust/core/debugger/lexer.rs +27 -27
- package/rust/core/debugger/logs.rs +52 -52
- package/rust/core/debugger/preprocessor.rs +27 -27
- package/rust/core/debugger/store.rs +38 -38
- package/rust/core/lexer/driver.rs +59 -59
- package/rust/core/lexer/handler/arrow.rs +82 -82
- package/rust/core/lexer/handler/at.rs +21 -21
- package/rust/core/lexer/handler/brace.rs +41 -41
- package/rust/core/lexer/handler/colon.rs +21 -21
- package/rust/core/lexer/handler/comment.rs +30 -30
- package/rust/core/lexer/handler/dot.rs +21 -21
- package/rust/core/lexer/handler/driver.rs +337 -337
- package/rust/core/lexer/handler/identifier.rs +47 -47
- package/rust/core/lexer/handler/indent.rs +66 -66
- package/rust/core/lexer/handler/mod.rs +15 -15
- package/rust/core/lexer/handler/newline.rs +23 -23
- package/rust/core/lexer/handler/number.rs +31 -31
- package/rust/core/lexer/handler/operator.rs +46 -46
- package/rust/core/lexer/handler/parenthesis.rs +41 -41
- package/rust/core/lexer/handler/slash.rs +21 -21
- package/rust/core/lexer/handler/string.rs +63 -63
- package/rust/core/lexer/mod.rs +3 -3
- package/rust/core/mod.rs +9 -9
- package/rust/core/parser/driver/block.rs +111 -111
- package/rust/core/parser/driver/cursor.rs +82 -82
- package/rust/core/parser/driver/driver_impl.rs +21 -1
- package/rust/core/parser/driver/mod.rs +6 -6
- package/rust/core/parser/driver/parse_array.rs +120 -120
- package/rust/core/parser/driver/parse_map.rs +247 -223
- package/rust/core/parser/driver/parser.rs +160 -160
- package/rust/core/parser/handler/arrow_call.rs +65 -14
- package/rust/core/parser/handler/identifier/synth.rs +171 -135
- package/rust/core/parser/handler/mod.rs +9 -9
- package/rust/core/parser/handler/pattern.rs +24 -1
- package/rust/core/plugin/loader.rs +137 -137
- package/rust/core/plugin/mod.rs +2 -2
- package/rust/core/plugin/runner/non_wasm.rs +481 -297
- package/rust/core/plugin/runner/wasm32.rs +1 -0
- package/rust/core/preprocessor/loader/inject.rs +313 -278
- package/rust/core/preprocessor/loader/loader_helpers.rs +110 -110
- package/rust/core/preprocessor/loader/mod.rs +235 -235
- package/rust/core/preprocessor/module.rs +55 -55
- package/rust/core/preprocessor/processor/handlers.rs +107 -107
- package/rust/core/preprocessor/resolver/bank.rs +49 -49
- package/rust/core/preprocessor/resolver/call.rs +124 -124
- package/rust/core/preprocessor/resolver/condition.rs +95 -95
- package/rust/core/preprocessor/resolver/driver.rs +324 -324
- package/rust/core/preprocessor/resolver/function.rs +69 -69
- package/rust/core/preprocessor/resolver/group.rs +122 -122
- package/rust/core/preprocessor/resolver/let_.rs +32 -32
- package/rust/core/preprocessor/resolver/loop_.rs +318 -318
- package/rust/core/preprocessor/resolver/mod.rs +16 -16
- package/rust/core/preprocessor/resolver/pattern.rs +95 -83
- package/rust/core/preprocessor/resolver/spawn.rs +99 -99
- package/rust/core/preprocessor/resolver/synth.rs +54 -54
- package/rust/core/preprocessor/resolver/tempo.rs +48 -48
- package/rust/core/preprocessor/resolver/trigger.rs +116 -116
- package/rust/core/preprocessor/resolver/value.rs +176 -176
- package/rust/core/store/global.rs +57 -57
- package/rust/lib.rs +323 -323
- package/rust/macros/Cargo.toml +14 -0
- package/rust/macros/src/lib.rs +52 -0
- package/rust/main.rs +311 -142
- package/rust/types/Cargo.toml +1 -1
- package/rust/types/src/addons.rs +3 -1
- package/rust/types/src/config.rs +1 -3
- package/rust/utils/Cargo.toml +5 -2
- package/rust/utils/src/file.rs +397 -14
- package/rust/utils/src/path.rs +31 -2
- package/rust/utils/src/version.rs +38 -7
- package/rust/web/auth.rs +5 -0
- package/rust/web/forge.rs +5 -0
- package/rust/web/mod.rs +5 -3
- package/typescript/scripts/version/copy-to-binary.ts +82 -0
- package/rust/cli/bank/api.rs +0 -122
- package/rust/cli/bank/commands.rs +0 -306
- package/rust/cli/bank/mod.rs +0 -29
- package/rust/cli/install/bank.rs +0 -72
- package/rust/cli/install/commands.rs +0 -35
- package/rust/cli/install/mod.rs +0 -4
- 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
|
+
}
|