@ionyx-apps/cli 0.2.1 → 0.3.1

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 (37) hide show
  1. package/bin/ionyx.exe +0 -0
  2. package/ionyx-cli/Cargo.toml +40 -0
  3. package/ionyx-cli/src/commands/build.rs +53 -0
  4. package/ionyx-cli/src/commands/bundle.rs +349 -0
  5. package/ionyx-cli/src/commands/config.rs +36 -0
  6. package/ionyx-cli/src/commands/dev.rs +142 -0
  7. package/ionyx-cli/src/commands/doctor.rs +158 -0
  8. package/ionyx-cli/src/commands/info.rs +79 -0
  9. package/ionyx-cli/src/commands/init.rs +64 -0
  10. package/ionyx-cli/src/commands/mod.rs +9 -0
  11. package/ionyx-cli/src/commands/plugin.rs +34 -0
  12. package/ionyx-cli/src/commands/run.rs +54 -0
  13. package/ionyx-cli/src/config/mod.rs +155 -0
  14. package/ionyx-cli/src/main.rs +161 -0
  15. package/ionyx-cli/src/templates/basic/src-ionyx/Cargo.toml +26 -20
  16. package/ionyx-cli/src/templates/basic/src-ionyx/src/lib.rs +6 -0
  17. package/ionyx-cli/src/templates/basic/src-ionyx/src/protocol.rs +5 -4
  18. package/ionyx-cli/src/templates/leptos/Cargo.toml +13 -13
  19. package/ionyx-cli/src/templates/leptos/src-ionyx/Cargo.toml +20 -20
  20. package/ionyx-cli/src/templates/leptos/src-ionyx/src/protocol.rs +5 -4
  21. package/ionyx-cli/src/templates/react/src-ionyx/Cargo.toml +26 -20
  22. package/ionyx-cli/src/templates/react/src-ionyx/src/lib.rs +6 -0
  23. package/ionyx-cli/src/templates/react/src-ionyx/src/protocol.rs +5 -4
  24. package/ionyx-cli/src/templates/src-ionyx/Cargo.toml +18 -18
  25. package/ionyx-cli/src/templates/svelte/src-ionyx/Cargo.toml +26 -20
  26. package/ionyx-cli/src/templates/svelte/src-ionyx/src/lib.rs +6 -0
  27. package/ionyx-cli/src/templates/svelte/src-ionyx/src/protocol.rs +5 -4
  28. package/ionyx-cli/src/templates/vanilla/src-ionyx/Cargo.toml +26 -20
  29. package/ionyx-cli/src/templates/vanilla/src-ionyx/src/lib.rs +6 -0
  30. package/ionyx-cli/src/templates/vanilla/src-ionyx/src/protocol.rs +5 -4
  31. package/ionyx-cli/src/templates/vue/src-ionyx/Cargo.toml +26 -20
  32. package/ionyx-cli/src/templates/vue/src-ionyx/src/lib.rs +6 -0
  33. package/ionyx-cli/src/templates/vue/src-ionyx/src/protocol.rs +5 -4
  34. package/package.json +12 -4
  35. package/bin/cargo-ionyx-win.exe +0 -0
  36. package/bin/cargo-ionyx.exe +0 -0
  37. package/ionyx-cli/src/templates/react/src-ionyx/Cargo.lock +0 -5536
package/bin/ionyx.exe CHANGED
Binary file
@@ -0,0 +1,40 @@
1
+ [package]
2
+ name = "ionyx-cli"
3
+ version = "0.3.1"
4
+ edition = "2021"
5
+ description = "Ionyx Framework CLI Tool"
6
+ authors = ["Ionyx Team"]
7
+ license = "MIT"
8
+ repository = "https://github.com/ionyx-framework/ionyx-cli"
9
+
10
+ [[bin]]
11
+ name = "ionyx"
12
+ path = "src/main.rs"
13
+
14
+ [dependencies]
15
+ clap = { version = "4.4", features = ["derive"] }
16
+ serde = { version = "1.0", features = ["derive"] }
17
+ serde_json = "1.0"
18
+ tokio = { version = "1.0", features = ["full"] }
19
+ anyhow = "1.0"
20
+ colored = "2.0"
21
+ dirs = "5.0"
22
+ handlebars = "4.5"
23
+ zip = "0.6"
24
+ reqwest = { version = "0.11", features = ["json"] }
25
+ futures = "0.3"
26
+ include_dir = "0.7"
27
+ dialoguer = "0.11"
28
+ os_info = "3.8"
29
+ toml = "0.8"
30
+ [workspace]
31
+ exclude = [
32
+ "src/templates/basic/src-ionyx",
33
+ "src/templates/leptos",
34
+ "src/templates/leptos/src-ionyx",
35
+ "src/templates/react/src-ionyx",
36
+ "src/templates/src-ionyx",
37
+ "src/templates/svelte/src-ionyx",
38
+ "src/templates/vanilla/src-ionyx",
39
+ "src/templates/vue/src-ionyx"
40
+ ]
@@ -0,0 +1,53 @@
1
+ use anyhow::Result;
2
+ use colored::*;
3
+ use tokio::process::Command;
4
+ use std::path::Path;
5
+
6
+ pub async fn execute(target: &str, minify: bool) -> Result<()> {
7
+ println!("šŸ”Ø Building Ionyx project for target: {}", target.cyan());
8
+
9
+ if !Path::new("package.json").exists() {
10
+ return Err(anyhow::anyhow!("No package.json found. Please run this command in an Ionyx project directory."));
11
+ }
12
+
13
+ // 1. Build Frontend
14
+ println!("🌐 Building frontend...");
15
+ let fe_status = if cfg!(target_os = "windows") {
16
+ Command::new("cmd")
17
+ .args(&["/C", "npm", "run", "build"])
18
+ .env("BUILD_TARGET", target)
19
+ .env("MINIFY", if minify { "true" } else { "false" })
20
+ .status()
21
+ .await?
22
+ } else {
23
+ Command::new("npm")
24
+ .args(&["run", "build"])
25
+ .env("BUILD_TARGET", target)
26
+ .env("MINIFY", if minify { "true" } else { "false" })
27
+ .status()
28
+ .await?
29
+ };
30
+
31
+ if !fe_status.success() {
32
+ return Err(anyhow::anyhow!("Frontend build failed"));
33
+ }
34
+
35
+ // 2. Build Backend
36
+ println!("šŸ¦€ Building Rust backend host...");
37
+ let mut be_cmd = Command::new("cargo");
38
+ be_cmd.args(&["build", "--release"]);
39
+
40
+ if Path::new("src-ionyx").exists() {
41
+ be_cmd.current_dir("src-ionyx");
42
+ }
43
+
44
+ let be_status = be_cmd.status().await?;
45
+ if !be_status.success() {
46
+ return Err(anyhow::anyhow!("Backend build failed"));
47
+ }
48
+
49
+ println!("\nāœ… Full build completed successfully!");
50
+ println!("šŸ“¦ Backend binary: src-ionyx/target/release/ionyx-host");
51
+
52
+ Ok(())
53
+ }
@@ -0,0 +1,349 @@
1
+ use anyhow::Result;
2
+ use colored::*;
3
+ use std::env;
4
+ use std::fs;
5
+ use std::path::{Path, PathBuf};
6
+ use tokio::process::Command;
7
+ use std::io::{Read, Write};
8
+ use zip::write::FileOptions;
9
+
10
+ pub async fn execute(platform: Option<String>, format: Option<String>) -> Result<()> {
11
+ let target_platform = platform.unwrap_or_else(|| env::consts::OS.to_string());
12
+ let target_format = format.unwrap_or_else(|| {
13
+ if target_platform == "windows" {
14
+ "nsis".to_string()
15
+ } else if target_platform == "macos" {
16
+ "dmg".to_string()
17
+ } else {
18
+ "appimage".to_string()
19
+ }
20
+ });
21
+
22
+ println!("šŸ“¦ Bundling Ionyx project for {}: {}", target_platform.cyan(), target_format.green());
23
+
24
+ let (app_name, app_version) = if Path::new("package.json").exists() {
25
+ let content = fs::read_to_string("package.json")?;
26
+ let pkg: serde_json::Value = serde_json::from_str(&content)?;
27
+ let name = pkg["name"].as_str().unwrap_or("ionyx-app").to_string();
28
+ let version = pkg["version"].as_str().unwrap_or("1.0.0").to_string();
29
+ (name, version)
30
+ } else {
31
+ ("ionyx-app".to_string(), "1.0.0".to_string())
32
+ };
33
+
34
+ let icon_path = if Path::new("ionyx.config.json").exists() {
35
+ let content = fs::read_to_string("ionyx.config.json")?;
36
+ let cfg: serde_json::Value = serde_json::from_str(&content)?;
37
+ cfg["window"]["icon"].as_str().map(|s| s.to_string())
38
+ } else {
39
+ None
40
+ };
41
+ let icon_path_str = icon_path.unwrap_or_else(|| "icon.png".to_string());
42
+
43
+ // 1. Build the project first
44
+ println!("šŸ”Ø Running full build...");
45
+ crate::commands::build::execute("all", true).await?;
46
+
47
+ // 2. Prepare installer directory
48
+ let installer_dir = Path::new("installers");
49
+ if installer_dir.exists() {
50
+ fs::remove_dir_all(installer_dir)?;
51
+ }
52
+ fs::create_dir_all(installer_dir)?;
53
+
54
+ // 3. Find the executable
55
+ let mut exe_path = PathBuf::from("src-ionyx/target/release");
56
+ if target_platform == "windows" {
57
+ exe_path.push("ionyx-host.exe");
58
+ } else {
59
+ exe_path.push("ionyx-host");
60
+ }
61
+
62
+ if !exe_path.exists() {
63
+ return Err(anyhow::anyhow!("Binary not found at {:?}. Did the build fail?", exe_path));
64
+ }
65
+
66
+ // 4. Platform-specific bundling
67
+ match target_platform.as_str() {
68
+ "windows" => {
69
+ if target_format == "nsis" {
70
+ bundle_windows_nsis(installer_dir, &exe_path, &app_name, &app_version, &icon_path_str).await?;
71
+ } else if target_format == "wix" {
72
+ bundle_windows_wix(installer_dir, &exe_path, &app_name, &app_version, &icon_path_str).await?;
73
+ } else {
74
+ return Err(anyhow::anyhow!("Unsupported format for windows: {}", target_format));
75
+ }
76
+ }
77
+ "macos" => {
78
+ bundle_macos_dmg(installer_dir, &exe_path, &app_name, &app_version, &icon_path_str).await?;
79
+ }
80
+ "linux" => {
81
+ bundle_linux_appimage(installer_dir, &exe_path, &app_name, &app_version, &icon_path_str).await?;
82
+ }
83
+ _ => return Err(anyhow::anyhow!("Unsupported platform: {}", target_platform)),
84
+ }
85
+
86
+ println!("\nšŸŽ‰ Bundling completed successfully!");
87
+ println!("šŸ“ Output directory: {}", "installers/".cyan());
88
+
89
+ Ok(())
90
+ }
91
+
92
+ async fn bundle_windows_nsis(installer_dir: &Path, exe_path: &Path, app_name: &str, app_version: &str, icon_path: &str) -> Result<()> {
93
+ println!("🪟 Building Windows EXE installer via NSIS...");
94
+
95
+ let icon_absolute = if Path::new(icon_path).exists() {
96
+ Some(fs::canonicalize(icon_path)?)
97
+ } else {
98
+ None
99
+ };
100
+
101
+ let nsis_script = format!(
102
+ r#"!define APP_NAME "{}"
103
+ !define APP_VERSION "{}"
104
+ !define APP_PUBLISHER "Ionyx Framework Team"
105
+ !define APP_EXE "ionyx-host.exe"
106
+ {}
107
+
108
+ Name "${{APP_NAME}}"
109
+ OutFile "{}\IonyxApp-Setup.exe"
110
+ InstallDir "$PROGRAMFILES64\${{APP_NAME}}"
111
+ RequestExecutionLevel admin
112
+
113
+ Page directory
114
+ Page instfiles
115
+
116
+ Section "MainSection" SEC01
117
+ SetOutPath "$INSTDIR"
118
+ File "{}"
119
+ CreateShortCut "$DESKTOP\${{APP_NAME}}.lnk" "$INSTDIR\${{APP_EXE}}" "" "{}" 0
120
+ SectionEnd
121
+ "#,
122
+ app_name,
123
+ app_version,
124
+ if let Some(ref icon) = icon_absolute {
125
+ format!("!define APP_ICON \"{}\"", icon.display())
126
+ } else {
127
+ "".to_string()
128
+ },
129
+ installer_dir.display(),
130
+ exe_path.canonicalize()?.display(),
131
+ icon_absolute.map(|p| p.display().to_string()).unwrap_or_default()
132
+ );
133
+
134
+ let script_path = installer_dir.join("installer.nsi");
135
+ fs::write(&script_path, nsis_script)?;
136
+
137
+ let status = Command::new("makensis")
138
+ .arg(&script_path)
139
+ .status()
140
+ .await;
141
+
142
+ match status {
143
+ Ok(s) if s.success() => println!("āœ… NSIS build successful"),
144
+ _ => {
145
+ println!("āš ļø NSIS (makensis) not found or failed. Falling back to structured ZIP.");
146
+ bundle_zip_fallback(installer_dir, exe_path, app_name, app_version).await?;
147
+ }
148
+ }
149
+
150
+ Ok(())
151
+ }
152
+
153
+ async fn bundle_windows_wix(installer_dir: &Path, exe_path: &Path, app_name: &str, _app_version: &str, _icon_path: &str) -> Result<()> {
154
+ println!("🪟 Building Windows MSI installer via WiX...");
155
+ // Placeholder for WiX implementation
156
+ Ok(())
157
+ }
158
+
159
+ async fn bundle_macos_dmg(installer_dir: &Path, exe_path: &Path, app_name: &str, app_version: &str, icon_path: &str) -> Result<()> {
160
+ println!("šŸŽ Building macOS DMG installer...");
161
+
162
+ let dmg_script = format!(
163
+ r#"#!/bin/bash
164
+ APP_NAME="{}"
165
+ TEMP_DIR="/tmp/${{APP_NAME}}"
166
+ APP_DIR="${{TEMP_DIR}}/${{APP_NAME}}.app"
167
+
168
+ mkdir -p "${{APP_DIR}}/Contents/MacOS"
169
+ mkdir -p "${{APP_DIR}}/Contents/Resources"
170
+
171
+ cp "{}" "${{APP_DIR}}/Contents/MacOS/ionyx-host"
172
+ {}
173
+
174
+ cat > "${{APP_DIR}}/Contents/Info.plist" << EOF
175
+ <?xml version="1.0" encoding="UTF-8"?>
176
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
177
+ <plist version="1.0">
178
+ <dict>
179
+ <key>CFBundleExecutable</key>
180
+ <string>ionyx-host</string>
181
+ <key>CFBundleIdentifier</key>
182
+ <string>com.ionyx-framework.app</string>
183
+ <key>CFBundleName</key>
184
+ <string>${{APP_NAME}}</string>
185
+ <key>CFBundleVersion</key>
186
+ <string>1.0.0</string>
187
+ {}
188
+ </dict>
189
+ </plist>
190
+ EOF
191
+
192
+ hdiutil create -volname "${{APP_NAME}}" -srcfolder "${{TEMP_DIR}}" -ov -format UDZO "{}/{}.dmg"
193
+ rm -rf "${{TEMP_DIR}}"
194
+ "#,
195
+ app_name,
196
+ exe_path.canonicalize()?.display(),
197
+ if Path::new(icon_path).exists() {
198
+ format!("cp \"{}\" \"${{APP_DIR}}/Contents/Resources/icon.png\"", icon_path)
199
+ } else {
200
+ "".to_string()
201
+ },
202
+ if Path::new(icon_path).exists() {
203
+ "<key>CFBundleIconFile</key><string>icon.png</string>".to_string()
204
+ } else {
205
+ "".to_string()
206
+ },
207
+ installer_dir.display(),
208
+ app_name
209
+ );
210
+
211
+ let script_path = installer_dir.join("build-dmg.sh");
212
+ fs::write(&script_path, dmg_script)?;
213
+
214
+ let status = Command::new("bash")
215
+ .arg(&script_path)
216
+ .status()
217
+ .await;
218
+
219
+ match status {
220
+ Ok(s) if s.success() => println!("āœ… DMG build successful"),
221
+ _ => {
222
+ println!("āš ļø macOS bundling tools not available or failed. Falling back to structured ZIP.");
223
+ bundle_zip_fallback(installer_dir, exe_path, app_name, app_version).await?;
224
+ }
225
+ }
226
+
227
+ Ok(())
228
+ }
229
+
230
+ async fn bundle_linux_appimage(installer_dir: &Path, exe_path: &Path, app_name: &str, app_version: &str, icon_path: &str) -> Result<()> {
231
+ println!("🐧 Building Linux AppImage...");
232
+
233
+ let appimage_script = format!(
234
+ r#"#!/bin/bash
235
+ APP_NAME="{}"
236
+ APP_DIR="{}/${{APP_NAME}}.AppImage"
237
+ TEMP_DIR="{}/temp"
238
+
239
+ mkdir -p "${{TEMP_DIR}}/usr/bin"
240
+ mkdir -p "${{TEMP_DIR}}/usr/share/icons/hicolor/256x256/apps"
241
+
242
+ cp "{}" "${{TEMP_DIR}}/usr/bin/ionyx-host"
243
+ chmod +x "${{TEMP_DIR}}/usr/bin/ionyx-host"
244
+
245
+ {}
246
+
247
+ cat > "${{TEMP_DIR}}/usr/share/applications/${{APP_NAME}}.desktop" << EOF
248
+ [Desktop Entry]
249
+ Name=${{APP_NAME}}
250
+ Exec=ionyx-host
251
+ Icon=icon
252
+ Type=Application
253
+ Categories=Utility;
254
+ EOF
255
+
256
+ cat > "${{TEMP_DIR}}/AppRun" << EOF
257
+ #!/bin/bash
258
+ HERE="\$(dirname "\$0")"
259
+ exec "\$HERE/usr/bin/ionyx-host"
260
+ EOF
261
+ chmod +x "${{TEMP_DIR}}/AppRun"
262
+
263
+ # Fallback to simple tar if appimagetool not found
264
+ if command -v appimagetool >/dev/null; then
265
+ appimagetool "${{TEMP_DIR}}" "${{APP_DIR}}"
266
+ else
267
+ tar -czf "{}/{}.tar.gz" -C "{}" temp
268
+ fi
269
+ rm -rf "${{TEMP_DIR}}"
270
+ "#,
271
+ app_name,
272
+ installer_dir.display(),
273
+ installer_dir.display(),
274
+ exe_path.canonicalize()?.display(),
275
+ if Path::new(icon_path).exists() {
276
+ format!("cp \"{}\" \"${{TEMP_DIR}}/usr/share/icons/hicolor/256x256/apps/icon.png\"", icon_path)
277
+ } else {
278
+ "".to_string()
279
+ },
280
+ installer_dir.display(),
281
+ app_name,
282
+ installer_dir.display()
283
+ );
284
+
285
+ let script_path = installer_dir.join("build-appimage.sh");
286
+ fs::write(&script_path, appimage_script)?;
287
+
288
+ let status = Command::new("bash")
289
+ .arg(&script_path)
290
+ .status()
291
+ .await;
292
+
293
+ match status {
294
+ Ok(s) if s.success() => println!("āœ… Linux bundling complete"),
295
+ _ => {
296
+ println!("āš ļø Linux bundling tools not available or failed. Falling back to structured ZIP.");
297
+ bundle_zip_fallback(installer_dir, exe_path, app_name, app_version).await?;
298
+ }
299
+ }
300
+
301
+ Ok(())
302
+ }
303
+
304
+ async fn bundle_zip_fallback(installer_dir: &Path, exe_path: &Path, app_name: &str, app_version: &str) -> Result<()> {
305
+ println!("šŸ“¦ Creating a professional portable ZIP distribution...");
306
+ let zip_name = format!("{}-{}-portable.zip", app_name, app_version);
307
+ let zip_path = installer_dir.join(zip_name);
308
+
309
+ let file = fs::File::create(&zip_path)?;
310
+ let mut zip = zip::ZipWriter::new(file);
311
+ let options = FileOptions::default()
312
+ .compression_method(zip::CompressionMethod::Deflated)
313
+ .unix_permissions(0o755);
314
+
315
+ // 1. Add Main Executable
316
+ let exe_name = if cfg!(target_os = "windows") {
317
+ format!("{}.exe", app_name)
318
+ } else {
319
+ app_name.to_string()
320
+ };
321
+
322
+ zip.start_file(format!("{}/{}", app_name, exe_name), options)?;
323
+ let mut exe_file = fs::File::open(exe_path)?;
324
+ let mut buffer = Vec::new();
325
+ exe_file.read_to_end(&mut buffer)?;
326
+ zip.write_all(&buffer)?;
327
+
328
+ // 2. Add Icon if exists
329
+ if Path::new("icon.png").exists() {
330
+ zip.start_file(format!("{}/icon.png", app_name), options)?;
331
+ let mut icon_file = fs::File::open("icon.png")?;
332
+ let mut icon_buffer = Vec::new();
333
+ icon_file.read_to_end(&mut icon_buffer)?;
334
+ zip.write_all(&icon_buffer)?;
335
+ }
336
+
337
+ // 3. Add README.md
338
+ zip.start_file(format!("{}/README.txt", app_name), options)?;
339
+ let readme_content = format!(
340
+ "{} v{}\n\nPortable distribution created by Ionyx Framework šŸš€\n\nTo run the application, execute: {}\n",
341
+ app_name, app_version, exe_name
342
+ );
343
+ zip.write_all(readme_content.as_bytes())?;
344
+
345
+ zip.finish()?;
346
+
347
+ println!("āœ… Structured portable ZIP created: {}", zip_path.display());
348
+ Ok(())
349
+ }
@@ -0,0 +1,36 @@
1
+ use anyhow::Result;
2
+ use colored::*;
3
+ use crate::config::ProjectConfig;
4
+ use std::fs;
5
+
6
+ pub async fn execute(action: crate::ConfigAction) -> Result<()> {
7
+ match action {
8
+ crate::ConfigAction::Get { key } => {
9
+ let config = ProjectConfig::load()?;
10
+ if let Some(value) = config.get(&key) {
11
+ println!("{} = {}", key.cyan(), value.green());
12
+ } else {
13
+ println!("{} = {}", key.cyan(), "not found".red());
14
+ }
15
+ }
16
+ crate::ConfigAction::Set { key, value } => {
17
+ let mut config = ProjectConfig::load()?;
18
+ config.set(&key, &value);
19
+ config.save()?;
20
+ println!("āœ… Set {} = {}", key.cyan(), value.green());
21
+ }
22
+ crate::ConfigAction::List => {
23
+ let config = ProjectConfig::load()?;
24
+ println!("šŸ“‹ Configuration:");
25
+ for (key, value) in config.get_all() {
26
+ println!(" {} = {}", key.cyan(), value.green().to_string());
27
+ }
28
+ }
29
+ crate::ConfigAction::Reset => {
30
+ ProjectConfig::reset()?;
31
+ println!("āœ… Configuration reset to defaults");
32
+ }
33
+ }
34
+
35
+ Ok(())
36
+ }
@@ -0,0 +1,142 @@
1
+ use anyhow::Result;
2
+ use colored::*;
3
+ use tokio::process::Command;
4
+ use std::path::Path;
5
+ use std::net::TcpStream;
6
+ use std::time::Duration;
7
+ use serde::Deserialize;
8
+ use tokio::fs;
9
+
10
+ #[derive(Deserialize)]
11
+ struct IonyxConfig {
12
+ build: Option<BuildConfig>,
13
+ }
14
+
15
+ #[derive(Deserialize)]
16
+ struct BuildConfig {
17
+ before_dev_command: Option<String>,
18
+ dev_url: Option<String>,
19
+ }
20
+
21
+ #[derive(Deserialize)]
22
+ struct CargoToml {
23
+ package: PackageConfig,
24
+ }
25
+
26
+ #[derive(Deserialize)]
27
+ struct PackageConfig {
28
+ name: String,
29
+ }
30
+
31
+ pub async fn execute(port: u16, _hot: bool) -> Result<()> {
32
+ println!("šŸ”„ Starting Ionyx development environment on port {}", port.to_string().cyan());
33
+
34
+ if !Path::new("package.json").exists() {
35
+ return Err(anyhow::anyhow!("No package.json found. Please run this command in an Ionyx project directory."));
36
+ }
37
+
38
+ // Read ionyx.config.toml
39
+ let config_path = "src-ionyx/ionyx.config.toml";
40
+ if !Path::new(config_path).exists() {
41
+ return Err(anyhow::anyhow!("No {} found. Please ensure you are in an Ionyx project.", config_path));
42
+ }
43
+ let config_content = fs::read_to_string(config_path).await?;
44
+ let config: IonyxConfig = toml::from_str(&config_content)?;
45
+
46
+ let before_dev_command = config.build.as_ref().and_then(|b| b.before_dev_command.clone()).unwrap_or_else(|| "npm run dev".to_string());
47
+ let dev_url = config.build.as_ref().and_then(|b| b.dev_url.clone()).unwrap_or_else(|| "http://127.0.0.1:5173".to_string());
48
+ let port = dev_url.split(':').last().and_then(|p| p.parse().ok()).unwrap_or(5173);
49
+
50
+ // Read Cargo.toml to get bin name
51
+ let cargo_path = "src-ionyx/Cargo.toml";
52
+ if !Path::new(cargo_path).exists() {
53
+ return Err(anyhow::anyhow!("No {} found.", cargo_path));
54
+ }
55
+ let cargo_content = fs::read_to_string(cargo_path).await?;
56
+ let cargo: CargoToml = toml::from_str(&cargo_content)?;
57
+ let bin_name = cargo.package.name;
58
+
59
+ // Check if node_modules exists, if not, install dependencies
60
+ if !Path::new("node_modules").exists() {
61
+ println!("šŸ“¦ Installing dependencies...");
62
+ let status = Command::new("npm")
63
+ .arg("install")
64
+ .status()
65
+ .await?;
66
+
67
+ if !status.success() {
68
+ return Err(anyhow::anyhow!("npm install failed"));
69
+ }
70
+ }
71
+
72
+ println!("🌐 Starting frontend dev server: {}", before_dev_command.cyan());
73
+ let cmd_parts: Vec<&str> = before_dev_command.split_whitespace().collect();
74
+ let mut fe_cmd = if cfg!(target_os = "windows") {
75
+ let mut c = Command::new("cmd");
76
+ c.args(&["/C"]).args(&cmd_parts);
77
+ c
78
+ } else {
79
+ let mut c = Command::new(cmd_parts[0]);
80
+ c.args(&cmd_parts[1..]);
81
+ c
82
+ };
83
+
84
+ let mut fe_child = fe_cmd.spawn()?;
85
+
86
+ // Wait for server to be ready
87
+ println!("ā³ Waiting for frontend server to be ready on port {}...", port);
88
+ let mut attempts = 0;
89
+ let max_attempts = 60; // 30 seconds total (60 * 500ms)
90
+
91
+ while attempts < max_attempts {
92
+ match TcpStream::connect(&format!("127.0.0.1:{}", port)) {
93
+ Ok(_) => {
94
+ println!("āœ… Frontend server is ready!");
95
+ break;
96
+ }
97
+ Err(_) => {
98
+ attempts += 1;
99
+ if attempts % 10 == 0 {
100
+ println!(" Still waiting... ({}s)", (attempts * 500) / 1000);
101
+ }
102
+ tokio::time::sleep(Duration::from_millis(500)).await;
103
+ }
104
+ }
105
+ }
106
+
107
+ if attempts >= max_attempts {
108
+ return Err(anyhow::anyhow!("Frontend server failed to start within 30 seconds"));
109
+ }
110
+
111
+ println!("šŸ¦€ Starting Rust backend host: {}", bin_name.cyan());
112
+ let mut be_cmd = Command::new("cargo");
113
+ be_cmd.args(&["run", "--bin", &bin_name]);
114
+ be_cmd.current_dir("src-ionyx");
115
+
116
+ // Set environment variable for backend
117
+ be_cmd.env("IONYX_URL", &format!("http://127.0.0.1:{}", port));
118
+
119
+ let mut be_child = be_cmd.spawn()?;
120
+
121
+ println!("\nšŸš€ Ionyx is running!");
122
+ println!(" Frontend: http://127.0.0.1:{}", port);
123
+ println!(" Backend: src-ionyx ({})", bin_name);
124
+ println!("\nPress Ctrl+C to stop both processes.\n");
125
+
126
+ tokio::select! {
127
+ res = fe_child.wait() => {
128
+ let status = res?;
129
+ println!("Frontend process exited with status: {}", status);
130
+ }
131
+ res = be_child.wait() => {
132
+ let status = res?;
133
+ println!("Backend process exited with status: {}", status);
134
+ }
135
+ }
136
+
137
+ // Kill the other process if one exits
138
+ let _ = fe_child.kill().await;
139
+ let _ = be_child.kill().await;
140
+
141
+ Ok(())
142
+ }