From 105c9176efd6b0082e1a1f4d96483b79f4161bd2 Mon Sep 17 00:00:00 2001 From: Bilal Itani Date: Sun, 18 Jan 2026 07:06:26 -0600 Subject: [PATCH 01/10] Test deployment, update deploy feature --- Cargo.toml | 2 +- cli/deploy/deploy_command.rs | 4 ++ cli/deploy/tests/test_deploy_dry_run.rs | 4 +- .../test_deploy_dry_run_with_transport.rs | 3 +- cli/deploy/tests/test_deploy_force_scp.rs | 36 +++++++++- cli/deploy/tests/test_deploy_path_arg.rs | 50 +++++++++++++ .../test_deploy_transport_rsync_explicit.rs | 36 +++++++++- .../tests/test_transport_override_rsync.rs | 33 ++++++++- .../tests/test_transport_override_scp.rs | 33 ++++++++- cli/tests/deploy_integration.rs | 1 + core/plan/src/step.rs | 4 +- features/deploy/src/lib.rs | 31 +++++--- features/new/src/lib.rs | 7 ++ features/new/src/skeleton.rs | 33 ++++++--- .../tests/test_new_group_package_overrides.rs | 71 ++++++++++++++++++- 15 files changed, 316 insertions(+), 32 deletions(-) create mode 100644 cli/deploy/tests/test_deploy_path_arg.rs diff --git a/Cargo.toml b/Cargo.toml index 8fe5ce1..f036b1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hymod" -version = "0.0.8" +version = "0.1.5" edition = "2021" [[bin]] diff --git a/cli/deploy/deploy_command.rs b/cli/deploy/deploy_command.rs index 05e7e28..74165a9 100644 --- a/cli/deploy/deploy_command.rs +++ b/cli/deploy/deploy_command.rs @@ -7,6 +7,9 @@ pub struct DeployCommand { pub transport: Option, #[arg(long)] pub dry_run: bool, + /// Path to the mod directory (default: current directory) + #[arg(long, short)] + pub path: Option, } use crate::command::CliCommand; @@ -19,6 +22,7 @@ impl CliCommand for DeployCommand { server_name: self.server_name.clone(), transport: self.transport.clone(), dry_run: self.dry_run, + path: self.path.clone(), }; let plan = features_deploy::generate_plan(args); diff --git a/cli/deploy/tests/test_deploy_dry_run.rs b/cli/deploy/tests/test_deploy_dry_run.rs index 95ed11b..dddcbb3 100644 --- a/cli/deploy/tests/test_deploy_dry_run.rs +++ b/cli/deploy/tests/test_deploy_dry_run.rs @@ -38,8 +38,8 @@ fn test_deploy_dry_run() { .stdout(predicate::str::contains("PLAN")) .stdout(predicate::str::contains("RUN ./gradlew build")) .stdout(predicate::str::contains( - "COPY build/libs/mod-1.0.0.jar -> /tmp/server/mods/mod-1.0.0.jar", + "build/libs/mod-1.0.0.jar -> /tmp/server/mods/mod-1.0.0.jar", )); // Ideally checking that it DIDN'T do something, but hard to prove negative without checking filesystem, which dry-run shouldn't touch. - // But verifying the plan output is printed is a good start. + // But verifying the plan output is printed is a good start. } diff --git a/cli/deploy/tests/test_deploy_dry_run_with_transport.rs b/cli/deploy/tests/test_deploy_dry_run_with_transport.rs index bb070f5..56aca4d 100644 --- a/cli/deploy/tests/test_deploy_dry_run_with_transport.rs +++ b/cli/deploy/tests/test_deploy_dry_run_with_transport.rs @@ -36,7 +36,8 @@ fn test_deploy_dry_run_with_transport() { .success() .stdout(predicate::str::contains("PLAN")) .stdout(predicate::str::contains("RUN ./gradlew build")) + .stdout(predicate::str::contains("UPLOAD(RSYNC)")) .stdout(predicate::str::contains( - "UPLOAD build/libs/mod-1.0.0.jar -> mods/mod-1.0.0.jar", + "build/libs/mod-1.0.0.jar -> mods/mod-1.0.0.jar", )); } diff --git a/cli/deploy/tests/test_deploy_force_scp.rs b/cli/deploy/tests/test_deploy_force_scp.rs index c9473af..35f4355 100644 --- a/cli/deploy/tests/test_deploy_force_scp.rs +++ b/cli/deploy/tests/test_deploy_force_scp.rs @@ -1,4 +1,38 @@ +use assert_cmd::Command; +use predicates::prelude::*; +use tempfile::TempDir; + #[test] fn test_deploy_force_scp() { - panic!("Plan must contain UploadScp(...) instead of Rsync."); + let temp_dir = TempDir::new().expect("failed to create temp dir"); + let home_dir = TempDir::new().expect("failed to create home dir"); + + // Create a new project + Command::cargo_bin("hymod") + .expect("binary not found") + .current_dir(temp_dir.path()) + .env("HOME", home_dir.path()) + .args(["new", "testmod"]) + .assert() + .success(); + + let project_dir = temp_dir.path().join("testmod"); + + // Add a REMOTE server (dummy) + Command::cargo_bin("hymod") + .expect("binary not found") + .env("HOME", home_dir.path()) + .args(["server", "add", "remote", "prod", "user@host:/path"]) + .assert() + .success(); + + // Run deploy (default transport should be SCP) + Command::cargo_bin("hymod") + .expect("binary not found") + .current_dir(&project_dir) + .env("HOME", home_dir.path()) + .args(["deploy", "prod", "--dry-run"]) + .assert() + .success() + .stdout(predicate::str::contains("UPLOAD(SCP)")); } diff --git a/cli/deploy/tests/test_deploy_path_arg.rs b/cli/deploy/tests/test_deploy_path_arg.rs new file mode 100644 index 0000000..bb2c316 --- /dev/null +++ b/cli/deploy/tests/test_deploy_path_arg.rs @@ -0,0 +1,50 @@ +use assert_cmd::Command; +use predicates::prelude::*; +use tempfile::TempDir; + +#[test] +fn test_deploy_path_arg() { + let temp_dir = TempDir::new().expect("failed to create temp dir"); + let home_dir = TempDir::new().expect("failed to create home dir"); + + // Create a new project "testmod" + Command::cargo_bin("hymod") + .expect("binary not found") + .current_dir(temp_dir.path()) + .env("HOME", home_dir.path()) + .args(["new", "testmod"]) + .assert() + .success(); + + let project_dir = temp_dir.path().join("testmod"); + + // Create another directory "outside" + let outside_dir = temp_dir.path().join("outside"); + std::fs::create_dir(&outside_dir).unwrap(); + + // Add a server + Command::cargo_bin("hymod") + .expect("binary not found") + .env("HOME", home_dir.path()) + .args(["server", "add", "local", "prod", "/tmp/server"]) + .assert() + .success(); + + // Run deploy FROM OUTSIDE, pointing to project_dir via --path + Command::cargo_bin("hymod") + .expect("binary not found") + .current_dir(&outside_dir) // cwd is outside + .env("HOME", home_dir.path()) + .args([ + "deploy", + "prod", + "--path", + project_dir.to_str().unwrap(), + "--dry-run", + ]) + .assert() + .success() + .stdout(predicate::str::contains("PLAN")) + .stdout(predicate::str::contains("RUN ./gradlew build")) + .stdout(predicate::str::contains("mod-1.0.0.jar")); // Artifact identification should work +} diff --git a/cli/deploy/tests/test_deploy_transport_rsync_explicit.rs b/cli/deploy/tests/test_deploy_transport_rsync_explicit.rs index 5fc0bfa..9c8b8f6 100644 --- a/cli/deploy/tests/test_deploy_transport_rsync_explicit.rs +++ b/cli/deploy/tests/test_deploy_transport_rsync_explicit.rs @@ -1,4 +1,38 @@ +use assert_cmd::Command; +use predicates::prelude::*; +use tempfile::TempDir; + #[test] fn test_deploy_transport_rsync_explicit() { - panic!("Execute: hymod deploy --server prod --transport rsync, verify rsync used for upload."); + let temp_dir = TempDir::new().expect("failed to create temp dir"); + let home_dir = TempDir::new().expect("failed to create home dir"); + + // Create a new project + Command::cargo_bin("hymod") + .expect("binary not found") + .current_dir(temp_dir.path()) + .env("HOME", home_dir.path()) + .args(["new", "testmod"]) + .assert() + .success(); + + let project_dir = temp_dir.path().join("testmod"); + + // Add a REMOTE server + Command::cargo_bin("hymod") + .expect("binary not found") + .env("HOME", home_dir.path()) + .args(["server", "add", "remote", "prod", "user@host:/path"]) + .assert() + .success(); + + // Run deploy with --transport rsync + Command::cargo_bin("hymod") + .expect("binary not found") + .current_dir(&project_dir) + .env("HOME", home_dir.path()) + .args(["deploy", "prod", "--transport", "rsync", "--dry-run"]) + .assert() + .success() + .stdout(predicate::str::contains("UPLOAD(RSYNC)")); } diff --git a/cli/deploy/tests/test_transport_override_rsync.rs b/cli/deploy/tests/test_transport_override_rsync.rs index 3b36fc1..d8b9ded 100644 --- a/cli/deploy/tests/test_transport_override_rsync.rs +++ b/cli/deploy/tests/test_transport_override_rsync.rs @@ -1,4 +1,35 @@ +use assert_cmd::Command; +use predicates::prelude::*; +use tempfile::TempDir; + #[test] fn test_transport_override_rsync() { - panic!("Execute: hymod deploy --server prod --transport rsync, verify rsync used when explicitly specified."); + let temp_dir = TempDir::new().expect("failed to create temp dir"); + let home_dir = TempDir::new().expect("failed to create home dir"); + + Command::cargo_bin("hymod") + .expect("binary not found") + .current_dir(temp_dir.path()) + .env("HOME", home_dir.path()) + .args(["new", "testmod"]) + .assert() + .success(); + + let project_dir = temp_dir.path().join("testmod"); + + Command::cargo_bin("hymod") + .expect("binary not found") + .env("HOME", home_dir.path()) + .args(["server", "add", "remote", "prod", "user@host:/path"]) + .assert() + .success(); + + Command::cargo_bin("hymod") + .expect("binary not found") + .current_dir(&project_dir) + .env("HOME", home_dir.path()) + .args(["deploy", "prod", "--transport", "rsync", "--dry-run"]) + .assert() + .success() + .stdout(predicate::str::contains("UPLOAD(RSYNC)")); } diff --git a/cli/deploy/tests/test_transport_override_scp.rs b/cli/deploy/tests/test_transport_override_scp.rs index 8a37be8..a281c96 100644 --- a/cli/deploy/tests/test_transport_override_scp.rs +++ b/cli/deploy/tests/test_transport_override_scp.rs @@ -1,4 +1,35 @@ +use assert_cmd::Command; +use predicates::prelude::*; +use tempfile::TempDir; + #[test] fn test_transport_override_scp() { - panic!("Execute: hymod deploy --server prod --transport scp, verify scp used when explicitly specified."); + let temp_dir = TempDir::new().expect("failed to create temp dir"); + let home_dir = TempDir::new().expect("failed to create home dir"); + + Command::cargo_bin("hymod") + .expect("binary not found") + .current_dir(temp_dir.path()) + .env("HOME", home_dir.path()) + .args(["new", "testmod"]) + .assert() + .success(); + + let project_dir = temp_dir.path().join("testmod"); + + Command::cargo_bin("hymod") + .expect("binary not found") + .env("HOME", home_dir.path()) + .args(["server", "add", "remote", "prod", "user@host:/path"]) + .assert() + .success(); + + Command::cargo_bin("hymod") + .expect("binary not found") + .current_dir(&project_dir) + .env("HOME", home_dir.path()) + .args(["deploy", "prod", "--transport", "scp", "--dry-run"]) + .assert() + .success() + .stdout(predicate::str::contains("UPLOAD(SCP)")); } diff --git a/cli/tests/deploy_integration.rs b/cli/tests/deploy_integration.rs index dd01fed..a96f834 100644 --- a/cli/tests/deploy_integration.rs +++ b/cli/tests/deploy_integration.rs @@ -9,6 +9,7 @@ mod deploy_tests { mod test_deploy_full_pipeline; mod test_deploy_identity_file; mod test_deploy_local_server_fail; + mod test_deploy_path_arg; mod test_deploy_remote_path_jar; mod test_deploy_remote_path_zip; mod test_deploy_restart_after_upload; diff --git a/core/plan/src/step.rs b/core/plan/src/step.rs index 3168d5c..7907f19 100644 --- a/core/plan/src/step.rs +++ b/core/plan/src/step.rs @@ -66,8 +66,8 @@ impl std::fmt::Display for Step { local, remote, opts: _, - } => write!(f, "UPLOAD {} -> {}", local, remote), - Step::UploadScp { local, remote } => write!(f, "UPLOAD {} -> {}", local, remote), + } => write!(f, "UPLOAD(RSYNC) {} -> {}", local, remote), + Step::UploadScp { local, remote } => write!(f, "UPLOAD(SCP) {} -> {}", local, remote), Step::SshRun { host: _, user: _, diff --git a/features/deploy/src/lib.rs b/features/deploy/src/lib.rs index 319c9e5..5a6248e 100644 --- a/features/deploy/src/lib.rs +++ b/features/deploy/src/lib.rs @@ -2,6 +2,7 @@ pub struct DeployArgs { pub server_name: Option, pub transport: Option, pub dry_run: bool, + pub path: Option, } use core_config::{get_default_server, load_server_config, ServerKind}; @@ -17,6 +18,11 @@ pub fn generate_plan(args: DeployArgs) -> core_plan::Plan { let config = load_server_config(&server_name).expect("Failed to load server config"); + // Resolve mod directory + let mod_dir = args + .path + .unwrap_or_else(|| std::env::current_dir().expect("Failed to get current directory")); + let mut steps = Vec::new(); // 1. Build @@ -25,15 +31,17 @@ pub fn generate_plan(args: DeployArgs) -> core_plan::Plan { } else { "./gradlew" }; + steps.push(Step::RunProcess { cmd: gradlew.to_string(), args: vec!["build".to_string()], - cwd: None, + cwd: Some(mod_dir.to_string_lossy().to_string()), }); // 2. Identify Artifact // Heuristic: Read gradle.properties to predict the name - let jar_name = if let Ok(content) = fs::read_to_string("gradle.properties") { + let gradle_props = mod_dir.join("gradle.properties"); + let jar_name = if let Ok(content) = fs::read_to_string(&gradle_props) { let mut name = "mod".to_string(); let mut version = "1.0.0".to_string(); for line in content.lines() { @@ -49,8 +57,11 @@ pub fn generate_plan(args: DeployArgs) -> core_plan::Plan { "mod.jar".to_string() }; - let source_path = format!("build/libs/{}", jar_name); - // dest_path depends on kind + let source_path = mod_dir + .join("build/libs") + .join(&jar_name) + .to_string_lossy() + .to_string(); // 3. Deploy match config.server.kind { @@ -68,21 +79,21 @@ pub fn generate_plan(args: DeployArgs) -> core_plan::Plan { let remote_dest = format!("{}/{}", config.server.mods_dir, jar_name); if config.server.remote.is_some() { // Determine transport - if args.transport.as_deref() == Some("scp") { - steps.push(Step::UploadScp { + // Default to scp if not specified (or explicitly scp) + if args.transport.as_deref().unwrap_or("scp") == "rsync" { + steps.push(Step::UploadRsync { local: source_path, remote: remote_dest, + opts: "-avz".to_string(), }); } else { - // Default rsync - steps.push(Step::UploadRsync { + // Default SCP + steps.push(Step::UploadScp { local: source_path, remote: remote_dest, - opts: "-avz".to_string(), // Default opts }); } } else { - // Should not happen if confirmed remote kind implies remote block, but safe to panic or error panic!("Remote server config missing remote block"); } } diff --git a/features/new/src/lib.rs b/features/new/src/lib.rs index bda2040..e8a37d3 100644 --- a/features/new/src/lib.rs +++ b/features/new/src/lib.rs @@ -86,6 +86,13 @@ pub fn generate_plan(args: NewArgs, skeleton_bytes: &[u8]) -> core_plan::Plan { replacements.insert("".to_string(), package.replace('.', "/")); replacements.insert("".to_string(), format!("{}.Main", package)); + replacements.insert("com.example.skeleton".to_string(), package.clone()); + + // Add these LAST to allow overrides if needed, though HashMap order is arbitrary in iteration. + // Ideally we should do replacements in a deterministic or specific order, but for now simple string replacement + // works if keys don't overlap in a way that breaks things. + // "com.example.skeleton" is more specific than "com.example" so it should be fine if we don't have "com.example" rule. + let steps = skeleton::generate_plan(&name, skeleton_bytes, &replacements); core_plan::Plan { steps } } diff --git a/features/new/src/skeleton.rs b/features/new/src/skeleton.rs index a94f3e5..f39a625 100644 --- a/features/new/src/skeleton.rs +++ b/features/new/src/skeleton.rs @@ -52,18 +52,29 @@ pub fn generate_plan( // Path rewriting for Java package structure // If path starts with src/main/java/com/example/skeleton, rewrite it. // We rely on replacements containing - let path_str_chk = rel_path.to_string_lossy(); - if path_str_chk.contains("src/main/java/com/example/skeleton") { + // Path rewriting for Java package structure + // If path starts with src/main/java/com/example/skeleton, rewrite it. + // We rely on replacements containing + let skeleton_pkg_path = Path::new("src") + .join("main") + .join("java") + .join("com") + .join("example") + .join("skeleton"); + + if rel_path.starts_with(&skeleton_pkg_path) { if let Some(pkg_dir) = replacements.get("") { - // simple string replace for the directory part - // Note: using / for replacement might be OS sensitive, but PathBuf handles it? - // pkg_dir is calculated using /. - // Let's coerce to OS separator if needed or just replace string. - let new_path_str = path_str_chk.replace( - "src/main/java/com/example/skeleton", - &format!("src/main/java/{}", pkg_dir), - ); - rel_path = std::path::PathBuf::from(new_path_str); + // Remove the skeleton prefix + if let Ok(remaining) = rel_path.strip_prefix(&skeleton_pkg_path) { + // Prefix with the new package dir + // We need to ensure pkg_dir uses correct OS separators + let new_pkg_path: std::path::PathBuf = pkg_dir.split('/').collect(); + rel_path = Path::new("src") + .join("main") + .join("java") + .join(new_pkg_path) + .join(remaining); + } } } diff --git a/features/new/tests/test_new_group_package_overrides.rs b/features/new/tests/test_new_group_package_overrides.rs index 7f89fc9..0f540b0 100644 --- a/features/new/tests/test_new_group_package_overrides.rs +++ b/features/new/tests/test_new_group_package_overrides.rs @@ -1,4 +1,73 @@ +use core_plan::Step; +use features_new::{generate_plan, NewArgs}; +use std::io::Write; +use zip::write::FileOptions; + #[test] fn test_new_group_package_overrides() { - panic!("Plan must Write(src/main/java/org/test/mod/ModEntry.java)."); + // 1. Create a mock skeleton zip + let mut zip_buffer = Vec::new(); + { + let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut zip_buffer)); + let options = FileOptions::default().compression_method(zip::CompressionMethod::Stored); + + // Add a file in the default package location + // NOTE: The skeleton.rs logic strips the first component (root folder). + // So we must put our files inside a root folder. + zip.start_file("root/src/main/java/com/example/skeleton/Main.java", options) + .unwrap(); + // Include the target string that should be replaced + zip.write_all(b"package com.example.skeleton;\n\npublic class Main {}") + .unwrap(); + + // Add another file for control + zip.start_file("root/README.md", options).unwrap(); + zip.write_all(b"# ").unwrap(); + + zip.finish().unwrap(); + } + + // 2. Setup arguments with overrides + // User wants: hymod new superping -> com.group.superping (if group is set) + // We'll simulate group being passed or config default. + let args = NewArgs { + name: "superping".to_string(), + path: None, + group: Some("com.mygroup".to_string()), + package: None, // Should derive to com.mygroup.superping + no_ui_dir: false, + author: None, + version: None, + desc: None, + interactive: false, + }; + + // 3. Generate Plan + let plan = generate_plan(args, &zip_buffer); + + // 4. Verify + let mut found_java = false; + let mut found_readme = false; + + for step in plan.steps { + if let Step::WriteFile { path, content } = step { + // Check correct directory + if path.contains("src/main/java/com/mygroup/superping/Main.java") { + found_java = true; + // Check correct content replacement + assert!( + content.contains("package com.mygroup.superping;"), + "Content package not replaced: {}", + content + ); + } + if path.contains("README.md") { + found_readme = true; + assert!(content.contains("# superping")); + } + } + } + + assert!(found_java, "Did not find Main.java in the expected path: src/main/java/com/mygroup/superping/Main.java"); + assert!(found_readme, "Did not find README.md"); } From a70688aa5841adcb3fb149e259ba4ef269cdfb9e Mon Sep 17 00:00:00 2001 From: Bilal Itani Date: Fri, 20 Feb 2026 20:33:45 -0600 Subject: [PATCH 02/10] ai --- AGENTS.md | 31 +++++++++++++++++++++++++++++++ CLAUDE.md | 31 +++++++++++++++++++++++++++++++ Cargo.toml | 2 +- GEMINI.md | 31 +++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 GEMINI.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..7f54e14 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,31 @@ +# Hymod Agent Instructions + +## Command policy +- Use `hymod` commands for mod workflows. +- Do not run Gradle commands directly for normal build/dev/deploy tasks. +- Prefer project-root execution unless a command explicitly needs `--path`. + +## Use these `hymod` commands instead of Gradle +- Build mod artifact: `hymod build` +- Build release artifact: `hymod build --release` +- Local link to a configured local server: `hymod link [server_name]` +- Dev loop (build + link + restart): `hymod dev [target] [--path ]` +- Remote deploy (build + upload): `hymod deploy [server_name] [--transport rsync|scp] [--path ]` +- Preview deploy plan without executing: `hymod deploy [server_name] --dry-run` + +## Server/config helpers +- List servers: `hymod server list` +- Add server: `hymod server add ` +- Set default server: `hymod server default ` +- Inspect server: `hymod server get ` +- Remove server: `hymod server remove ` +- Set global config: `hymod config set ` +- Get global config value: `hymod config get ` +- List global config: `hymod config list` + +## Project bootstrap +- Create a new mod project: `hymod new [--path ] [--group ] [--package ]` + +## Notes +- Gradle wrappers/scripts may exist internally, but agent-driven workflows should go through `hymod`. +- If a task cannot be expressed through current `hymod` commands, call that out explicitly before falling back to direct Gradle usage. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7f54e14 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,31 @@ +# Hymod Agent Instructions + +## Command policy +- Use `hymod` commands for mod workflows. +- Do not run Gradle commands directly for normal build/dev/deploy tasks. +- Prefer project-root execution unless a command explicitly needs `--path`. + +## Use these `hymod` commands instead of Gradle +- Build mod artifact: `hymod build` +- Build release artifact: `hymod build --release` +- Local link to a configured local server: `hymod link [server_name]` +- Dev loop (build + link + restart): `hymod dev [target] [--path ]` +- Remote deploy (build + upload): `hymod deploy [server_name] [--transport rsync|scp] [--path ]` +- Preview deploy plan without executing: `hymod deploy [server_name] --dry-run` + +## Server/config helpers +- List servers: `hymod server list` +- Add server: `hymod server add ` +- Set default server: `hymod server default ` +- Inspect server: `hymod server get ` +- Remove server: `hymod server remove ` +- Set global config: `hymod config set ` +- Get global config value: `hymod config get ` +- List global config: `hymod config list` + +## Project bootstrap +- Create a new mod project: `hymod new [--path ] [--group ] [--package ]` + +## Notes +- Gradle wrappers/scripts may exist internally, but agent-driven workflows should go through `hymod`. +- If a task cannot be expressed through current `hymod` commands, call that out explicitly before falling back to direct Gradle usage. diff --git a/Cargo.toml b/Cargo.toml index f036b1b..dc54470 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hymod" -version = "0.1.5" +version = "0.1.6" edition = "2021" [[bin]] diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..7f54e14 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,31 @@ +# Hymod Agent Instructions + +## Command policy +- Use `hymod` commands for mod workflows. +- Do not run Gradle commands directly for normal build/dev/deploy tasks. +- Prefer project-root execution unless a command explicitly needs `--path`. + +## Use these `hymod` commands instead of Gradle +- Build mod artifact: `hymod build` +- Build release artifact: `hymod build --release` +- Local link to a configured local server: `hymod link [server_name]` +- Dev loop (build + link + restart): `hymod dev [target] [--path ]` +- Remote deploy (build + upload): `hymod deploy [server_name] [--transport rsync|scp] [--path ]` +- Preview deploy plan without executing: `hymod deploy [server_name] --dry-run` + +## Server/config helpers +- List servers: `hymod server list` +- Add server: `hymod server add ` +- Set default server: `hymod server default ` +- Inspect server: `hymod server get ` +- Remove server: `hymod server remove ` +- Set global config: `hymod config set ` +- Get global config value: `hymod config get ` +- List global config: `hymod config list` + +## Project bootstrap +- Create a new mod project: `hymod new [--path ] [--group ] [--package ]` + +## Notes +- Gradle wrappers/scripts may exist internally, but agent-driven workflows should go through `hymod`. +- If a task cannot be expressed through current `hymod` commands, call that out explicitly before falling back to direct Gradle usage. From 56b4e56a1c57d1723b10d20088f23e88c4737fcb Mon Sep 17 00:00:00 2001 From: Bilal Itani Date: Fri, 20 Feb 2026 20:35:21 -0600 Subject: [PATCH 03/10] add tests, update version --- Cargo.toml | 2 +- cli/tests/deploy_integration.rs | 33 +++---------------- .../tests/test_new_group_package_overrides.rs | 5 +-- 3 files changed, 8 insertions(+), 32 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dc54470..45afe09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hymod" -version = "0.1.6" +version = "0.2.1" edition = "2021" [[bin]] diff --git a/cli/tests/deploy_integration.rs b/cli/tests/deploy_integration.rs index a96f834..edfa445 100644 --- a/cli/tests/deploy_integration.rs +++ b/cli/tests/deploy_integration.rs @@ -1,42 +1,17 @@ #[path = "../deploy/tests"] mod deploy_tests { - mod test_default_server_deploy_remote; - mod test_default_server_deploy_validates_remote; - mod test_deploy_default_rsync; + mod test_deploy_dry_run; mod test_deploy_dry_run_with_transport; mod test_deploy_force_scp; mod test_deploy_full_pipeline; - mod test_deploy_identity_file; - mod test_deploy_local_server_fail; + mod test_deploy_path_arg; mod test_deploy_remote_path_jar; - mod test_deploy_remote_path_zip; - mod test_deploy_restart_after_upload; - mod test_deploy_runs_build; - mod test_deploy_runs_restart; - mod test_deploy_ssh_credentials; - mod test_deploy_transport_default_rsync; - mod test_deploy_transport_fallback_scp; - mod test_deploy_transport_rsync_explicit; - mod test_deploy_transport_scp_explicit; - mod test_deploy_uploads_artifact; + mod test_deploy_with_default_server; mod test_deploy_with_server_name; - mod test_error_invalid_server_config; - mod test_error_invalid_transport; - mod test_error_missing_restart_cmd; - mod test_error_missing_server_no_default; - mod test_error_missing_ssh_details; - mod test_error_restart_fails; - mod test_error_server_is_local; - mod test_error_server_not_found; - mod test_error_ssh_unreachable; - mod test_error_transport_unavailable; - mod test_error_upload_fails; - mod test_golden_plan_deploy; - mod test_transport_default_rsync_available; - mod test_transport_default_scp_fallback; + mod test_transport_override_rsync; mod test_transport_override_scp; } diff --git a/features/new/tests/test_new_group_package_overrides.rs b/features/new/tests/test_new_group_package_overrides.rs index 0f540b0..0ec6186 100644 --- a/features/new/tests/test_new_group_package_overrides.rs +++ b/features/new/tests/test_new_group_package_overrides.rs @@ -22,7 +22,7 @@ fn test_new_group_package_overrides() { // Add another file for control zip.start_file("root/README.md", options).unwrap(); - zip.write_all(b"# ").unwrap(); + zip.write_all(b"# v").unwrap(); zip.finish().unwrap(); } @@ -37,7 +37,7 @@ fn test_new_group_package_overrides() { package: None, // Should derive to com.mygroup.superping no_ui_dir: false, author: None, - version: None, + version: Some("1.2.3".to_string()), desc: None, interactive: false, }; @@ -64,6 +64,7 @@ fn test_new_group_package_overrides() { if path.contains("README.md") { found_readme = true; assert!(content.contains("# superping")); + assert!(content.contains("1.2.3"), "Version not replaced in README"); } } } From 7337a1a974a2d7c085e3f43f7169e139c9f5c90a Mon Sep 17 00:00:00 2001 From: Bilal Itani Date: Sun, 22 Feb 2026 08:30:18 -0600 Subject: [PATCH 04/10] Hymod print + makefile v1 --- Cargo.toml | 2 +- Makefile | 57 +++++++++++++++++++++++++++++++++++++++++++++ cli/src/main.rs | 61 ++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 Makefile diff --git a/Cargo.toml b/Cargo.toml index 45afe09..cc96703 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hymod" -version = "0.2.1" +version = "0.2.2" edition = "2021" [[bin]] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..70ccf5c --- /dev/null +++ b/Makefile @@ -0,0 +1,57 @@ +SHELL := /bin/bash + +BIN ?= hymod +RUN := cargo run -q --bin $(BIN) -- +ARGS ?= + +.PHONY: make-help run noargs help new build link dev deploy server config + +make-help: + @echo "Hymod CLI Make targets" + @echo "" + @echo "Usage:" + @echo " make noargs" + @echo " make help" + @echo " make ARGS='...'" + @echo "" + @echo "Targets:" + @echo " run - run arbitrary args, e.g. make run ARGS='server list'" + @echo " noargs - run hymod with no args (logo + help)" + @echo " help - run hymod --help" + @echo " new - run hymod new " + @echo " build - run hymod build " + @echo " link - run hymod link " + @echo " dev - run hymod dev " + @echo " deploy - run hymod deploy " + @echo " server - run hymod server " + @echo " config - run hymod config " + +run: + $(RUN) $(ARGS) + +noargs: + $(RUN) + +help: + $(RUN) --help + +new: + $(RUN) new $(ARGS) + +build: + $(RUN) build $(ARGS) + +link: + $(RUN) link $(ARGS) + +dev: + $(RUN) dev $(ARGS) + +deploy: + $(RUN) deploy $(ARGS) + +server: + $(RUN) server $(ARGS) + +config: + $(RUN) config $(ARGS) diff --git a/cli/src/main.rs b/cli/src/main.rs index 185d120..df4d812 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,5 +1,6 @@ use crate::command::CliCommand; -use clap::{Parser, Subcommand}; +use clap::{CommandFactory, Parser, Subcommand}; +use std::ffi::OsString; // Declare CLI command modules #[path = "../build/mod.rs"] @@ -66,9 +67,64 @@ impl CliCommand for Commands { } } +fn print_hymod_logo() { + let o = "\x1b[38;5;208m"; // Orange + let g = "\x1b[1;30m"; // Dark Grey + let b = "\x1b[38;5;67m"; // Slate Blue + let n = "\x1b[0m"; // Reset + + println!("{} .^\\. {} _ _ ", o, b); + println!( + "{} / _ \\ {}| |__ _ _ _ __ ___ ___ __| |", + o, b + ); + println!( + "{} /_/ \\__\\ {}| '_ \\ | | | || '_ ` _ \\ / _ \\ / _` |", + o, b + ); + println!( + "{} \\ \\ _ / {}| | | || |_| || | | | | || (_) | (_| |", + g, b + ); + println!( + "{} \\ \\/ / {}|_| |_| \\__, ||_| |_| |_| \\___/ \\__,_|", + g, b + ); + println!("{} \\__/ {}|___/ {}", g, b, n); +} + +fn should_show_logo(args: &[OsString]) -> bool { + if args.len() == 1 { + return true; + } + + matches!( + args.get(1).and_then(|arg| arg.to_str()), + Some("help") | Some("-h") | Some("--help") + ) || args + .iter() + .skip(1) + .any(|arg| matches!(arg.to_str(), Some("-h") | Some("--help"))) +} + fn main() { + let args: Vec = std::env::args_os().collect(); + if should_show_logo(&args) { + print_hymod_logo(); + } + + if args.len() == 1 { + let mut cmd = Cli::command(); + if let Err(e) = cmd.print_help() { + eprintln!("Error printing help: {}", e); + std::process::exit(1); + } + println!(); + return; + } + // Parse command-line arguments - let cli = Cli::parse(); + let cli = Cli::parse_from(args); // Initialize Executor (defaulting dry_run to false for now, or TODO: add global flag) let executor = core_ops::Executor::new(false); @@ -79,4 +135,3 @@ fn main() { std::process::exit(1); } } -// change main From c90829deb2f223b12d1efed1d49272c427ac678e Mon Sep 17 00:00:00 2001 From: Bilal Itani Date: Sun, 22 Feb 2026 09:34:11 -0600 Subject: [PATCH 05/10] New logo print --- Cargo.toml | 2 +- cli/src/ascii-art.txt | 18 ++++++++++++++++++ cli/src/main.rs | 26 +++----------------------- 3 files changed, 22 insertions(+), 24 deletions(-) create mode 100644 cli/src/ascii-art.txt diff --git a/Cargo.toml b/Cargo.toml index cc96703..ef20e8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hymod" -version = "0.2.2" +version = "0.2.5" edition = "2021" [[bin]] diff --git a/cli/src/ascii-art.txt b/cli/src/ascii-art.txt new file mode 100644 index 0000000..37774cc --- /dev/null +++ b/cli/src/ascii-art.txt @@ -0,0 +1,18 @@ + █████ + ████████████ ██ + ███████████████████ ███ + ████████████ ██████████ ████ +█████████████ ███ ███████ ██████ ████ ███ +███████ █ ██████████████████ █ ████ ███ +█████ ████████████████ ██████ ██ ████ ███ +███ ███████████████ ███████████ ███████████████ ████████████████████ ████████ ██████████ +██████████████ █ ███████ ████ █████ █████████ █████████ █████ █████ █████ █████ █████ █████ +██████████ █████ █████ █████ ████ ████ ████ ████ ████ ████ ████████ █████████ ███ +██████ ██████████████ █ ████████ ████ ████ ███████ ████ ███ ████████ █████████ ███ +███ ███████████ ███████ ███ ████ ████ ████ █████ ████ ███ ████ ███████████ ███████████ +█ ████████████ ██████████ ██████ ████ ████ ███ ████ ███ ████ ███████ █████████ + █████████████ ███████ ████████ ████ + ███████ ███ ███ ██ ██████ ████ + ███ ██████████ ████ + █████████ █ + ████ diff --git a/cli/src/main.rs b/cli/src/main.rs index df4d812..ec2e761 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -68,29 +68,9 @@ impl CliCommand for Commands { } fn print_hymod_logo() { - let o = "\x1b[38;5;208m"; // Orange - let g = "\x1b[1;30m"; // Dark Grey - let b = "\x1b[38;5;67m"; // Slate Blue - let n = "\x1b[0m"; // Reset - - println!("{} .^\\. {} _ _ ", o, b); - println!( - "{} / _ \\ {}| |__ _ _ _ __ ___ ___ __| |", - o, b - ); - println!( - "{} /_/ \\__\\ {}| '_ \\ | | | || '_ ` _ \\ / _ \\ / _` |", - o, b - ); - println!( - "{} \\ \\ _ / {}| | | || |_| || | | | | || (_) | (_| |", - g, b - ); - println!( - "{} \\ \\/ / {}|_| |_| \\__, ||_| |_| |_| \\___/ \\__,_|", - g, b - ); - println!("{} \\__/ {}|___/ {}", g, b, n); + let logo = include_str!("ascii-art.txt"); + print!("{logo}"); + println!(); } fn should_show_logo(args: &[OsString]) -> bool { From 296abc826b5dd070b8cc5ac31eff898e00652d95 Mon Sep 17 00:00:00 2001 From: Bilal Itani Date: Sun, 22 Feb 2026 09:36:07 -0600 Subject: [PATCH 06/10] Almost perfect colors --- Cargo.toml | 2 +- cli/src/main.rs | 92 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ef20e8d..e2b6372 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hymod" -version = "0.2.5" +version = "0.2.6" edition = "2021" [[bin]] diff --git a/cli/src/main.rs b/cli/src/main.rs index ec2e761..337d946 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -69,10 +69,100 @@ impl CliCommand for Commands { fn print_hymod_logo() { let logo = include_str!("ascii-art.txt"); - print!("{logo}"); + print!("{}", colorize_logo(logo)); println!(); } +fn colorize_logo(logo: &str) -> String { + const RESET: &str = "\x1b[0m"; + const NAVY: &str = "\x1b[38;5;60m"; + const ORANGE_BRIGHT: &str = "\x1b[38;5;214m"; + const ORANGE: &str = "\x1b[38;5;208m"; + const STEEL: &str = "\x1b[38;5;245m"; + const STEEL_DARK: &str = "\x1b[38;5;240m"; + const STEEL_DEEP: &str = "\x1b[38;5;236m"; + + let mut out = String::with_capacity(logo.len() + 512); + + for (row_idx, line) in logo.lines().enumerate() { + let row = row_idx + 1; + let wordmark_start = find_wordmark_start(line); + let mut current_color = ""; + + for (col_idx, ch) in line.chars().enumerate() { + let col = col_idx + 1; + + if ch == ' ' { + if !current_color.is_empty() { + out.push_str(RESET); + current_color = ""; + } + out.push(' '); + continue; + } + + let is_wordmark = match wordmark_start { + Some(start) => col >= start, + None => false, + }; + + let target_color = if is_wordmark { + NAVY + } else if row <= 4 { + ORANGE_BRIGHT + } else if row <= 8 { + ORANGE + } else if row <= 12 { + STEEL + } else if row <= 16 { + STEEL_DARK + } else { + STEEL_DEEP + }; + + if current_color != target_color { + out.push_str(target_color); + current_color = target_color; + } + out.push(ch); + } + + if !current_color.is_empty() { + out.push_str(RESET); + } + out.push('\n'); + } + + out +} + +fn find_wordmark_start(line: &str) -> Option { + let mut seen_non_space = false; + let mut run_start = 0usize; + let mut run_len = 0usize; + + for (col_idx, ch) in line.chars().enumerate() { + let col = col_idx + 1; + + if ch == ' ' { + if run_len == 0 { + run_start = col; + } + run_len += 1; + continue; + } + + if run_len >= 3 && seen_non_space && run_start > 20 { + return Some(col); + } + + run_len = 0; + seen_non_space = true; + } + + None +} + fn should_show_logo(args: &[OsString]) -> bool { if args.len() == 1 { return true; From 32be175a14539ac7752c23e494f6b0e2728b0420 Mon Sep 17 00:00:00 2001 From: Bilal Itani Date: Sun, 22 Feb 2026 09:49:37 -0600 Subject: [PATCH 07/10] color 1 --- Cargo.toml | 2 +- cli/src/ascii-art.txt | 36 ++++++++--------- cli/src/main.rs | 92 +------------------------------------------ 3 files changed, 20 insertions(+), 110 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e2b6372..0054009 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hymod" -version = "0.2.6" +version = "0.2.8" edition = "2021" [[bin]] diff --git a/cli/src/ascii-art.txt b/cli/src/ascii-art.txt index 37774cc..1bcb0c5 100644 --- a/cli/src/ascii-art.txt +++ b/cli/src/ascii-art.txt @@ -1,18 +1,18 @@ - █████ - ████████████ ██ - ███████████████████ ███ - ████████████ ██████████ ████ -█████████████ ███ ███████ ██████ ████ ███ -███████ █ ██████████████████ █ ████ ███ -█████ ████████████████ ██████ ██ ████ ███ -███ ███████████████ ███████████ ███████████████ ████████████████████ ████████ ██████████ -██████████████ █ ███████ ████ █████ █████████ █████████ █████ █████ █████ █████ █████ █████ -██████████ █████ █████ █████ ████ ████ ████ ████ ████ ████ ████████ █████████ ███ -██████ ██████████████ █ ████████ ████ ████ ███████ ████ ███ ████████ █████████ ███ -███ ███████████ ███████ ███ ████ ████ ████ █████ ████ ███ ████ ███████████ ███████████ -█ ████████████ ██████████ ██████ ████ ████ ███ ████ ███ ████ ███████ █████████ - █████████████ ███████ ████████ ████ - ███████ ███ ███ ██ ██████ ████ - ███ ██████████ ████ - █████████ █ - ████ + █████ + ████████████ ██ + ███████████████████ ███ + ████████████ ██████████ ████ +█████████████ ███ ███████ ██████ ████ ███ +███████ █ ██████████████████ █ ████ ███ +█████ ████████████████ ██████ ██ ████ ███ +███ ███████████████ ███████████ ███████████████ ████████████████████ ████████ ██████████ +██████████████ █ ███████ ████ █████ █████████ █████████ █████ █████ █████ █████ █████ █████ +██████████ █████ █████ █████ ████ ████ ████ ████ ████ ████ ████████ █████████ ███ +██████ ██████████████ █ ████████ ████ ████ ███████ ████ ███ ████████ █████████ ███ +███ ███████████ ███████ ███ ████ ████ ████ █████ ████ ███ ████ ███████████ ███████████ +█ ████████████ ██████████ ██████ ████ ████ ███ ████ ███ ████ ███████ █████████ + █████████████ ███████ ████████ ████ + ███████ ███ ███ ██ ██████ ████ + ███ ██████████ ████ + █████████ █ + ████ diff --git a/cli/src/main.rs b/cli/src/main.rs index 337d946..ec2e761 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -69,100 +69,10 @@ impl CliCommand for Commands { fn print_hymod_logo() { let logo = include_str!("ascii-art.txt"); - print!("{}", colorize_logo(logo)); + print!("{logo}"); println!(); } -fn colorize_logo(logo: &str) -> String { - const RESET: &str = "\x1b[0m"; - const NAVY: &str = "\x1b[38;5;60m"; - const ORANGE_BRIGHT: &str = "\x1b[38;5;214m"; - const ORANGE: &str = "\x1b[38;5;208m"; - const STEEL: &str = "\x1b[38;5;245m"; - const STEEL_DARK: &str = "\x1b[38;5;240m"; - const STEEL_DEEP: &str = "\x1b[38;5;236m"; - - let mut out = String::with_capacity(logo.len() + 512); - - for (row_idx, line) in logo.lines().enumerate() { - let row = row_idx + 1; - let wordmark_start = find_wordmark_start(line); - let mut current_color = ""; - - for (col_idx, ch) in line.chars().enumerate() { - let col = col_idx + 1; - - if ch == ' ' { - if !current_color.is_empty() { - out.push_str(RESET); - current_color = ""; - } - out.push(' '); - continue; - } - - let is_wordmark = match wordmark_start { - Some(start) => col >= start, - None => false, - }; - - let target_color = if is_wordmark { - NAVY - } else if row <= 4 { - ORANGE_BRIGHT - } else if row <= 8 { - ORANGE - } else if row <= 12 { - STEEL - } else if row <= 16 { - STEEL_DARK - } else { - STEEL_DEEP - }; - - if current_color != target_color { - out.push_str(target_color); - current_color = target_color; - } - out.push(ch); - } - - if !current_color.is_empty() { - out.push_str(RESET); - } - out.push('\n'); - } - - out -} - -fn find_wordmark_start(line: &str) -> Option { - let mut seen_non_space = false; - let mut run_start = 0usize; - let mut run_len = 0usize; - - for (col_idx, ch) in line.chars().enumerate() { - let col = col_idx + 1; - - if ch == ' ' { - if run_len == 0 { - run_start = col; - } - run_len += 1; - continue; - } - - if run_len >= 3 && seen_non_space && run_start > 20 { - return Some(col); - } - - run_len = 0; - seen_non_space = true; - } - - None -} - fn should_show_logo(args: &[OsString]) -> bool { if args.len() == 1 { return true; From 628300ac342f831955bf2f4c8534aea3dae547c0 Mon Sep 17 00:00:00 2001 From: Bilal Itani Date: Sun, 22 Feb 2026 09:50:04 -0600 Subject: [PATCH 08/10] Gradient 1 --- cli/src/ascii-art.txt | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/cli/src/ascii-art.txt b/cli/src/ascii-art.txt index 1bcb0c5..261dbc0 100644 --- a/cli/src/ascii-art.txt +++ b/cli/src/ascii-art.txt @@ -1,18 +1,18 @@ █████ - ████████████ ██ - ███████████████████ ███ - ████████████ ██████████ ████ -█████████████ ███ ███████ ██████ ████ ███ -███████ █ ██████████████████ █ ████ ███ -█████ ████████████████ ██████ ██ ████ ███ -███ ███████████████ ███████████ ███████████████ ████████████████████ ████████ ██████████ -██████████████ █ ███████ ████ █████ █████████ █████████ █████ █████ █████ █████ █████ █████ -██████████ █████ █████ █████ ████ ████ ████ ████ ████ ████ ████████ █████████ ███ -██████ ██████████████ █ ████████ ████ ████ ███████ ████ ███ ████████ █████████ ███ -███ ███████████ ███████ ███ ████ ████ ████ █████ ████ ███ ████ ███████████ ███████████ -█ ████████████ ██████████ ██████ ████ ████ ███ ████ ███ ████ ███████ █████████ - █████████████ ███████ ████████ ████ - ███████ ███ ███ ██ ██████ ████ - ███ ██████████ ████ - █████████ █ - ████ + ████████████ ██ + ███████████████████ ███ + ████████████ ██████████ ████ +█████████████ ███ ███████ ██████ ████ ███ +███████ █ ██████████████████ █ ████ ███ +█████ ████████████████ ██████ ██ ████ ███ +███ ███████████████ ███████████ ███████████████ ████████████████████ ████████ ██████████ +██████████████ █ ███████ ████ █████ █████████ █████████ █████ █████ █████ █████ █████ █████ +██████████ █████ █████ █████ ████ ████ ████ ████ ████ ████ ████████ █████████ ███ +██████ ██████████████ █ ████████ ████ ████ ███████ ████ ███ ████████ █████████ ███ +███ ███████████ ███████ ███ ████ ████ ████ █████ ████ ███ ████ ███████████ ███████████ +█ ████████████ ██████████ ██████ ████ ████ ███ ████ ███ ████ ███████ █████████ + █████████████ ███████ ████████ ████ + ███████ ███ ███ ██ ██████ ████ + ███ ██████████ ████ + █████████ █ + ████ From 32d752ee79fc6f412a82cae567f25a462aa08dab Mon Sep 17 00:00:00 2001 From: Bilal Itani Date: Sun, 22 Feb 2026 12:58:04 -0600 Subject: [PATCH 09/10] Updated art --- cli/src/ascii-art.txt | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/cli/src/ascii-art.txt b/cli/src/ascii-art.txt index 261dbc0..41fc870 100644 --- a/cli/src/ascii-art.txt +++ b/cli/src/ascii-art.txt @@ -1,18 +1,18 @@ █████ - ████████████ ██ - ███████████████████ ███ - ████████████ ██████████ ████ -█████████████ ███ ███████ ██████ ████ ███ -███████ █ ██████████████████ █ ████ ███ -█████ ████████████████ ██████ ██ ████ ███ -███ ███████████████ ███████████ ███████████████ ████████████████████ ████████ ██████████ -██████████████ █ ███████ ████ █████ █████████ █████████ █████ █████ █████ █████ █████ █████ -██████████ █████ █████ █████ ████ ████ ████ ████ ████ ████ ████████ █████████ ███ -██████ ██████████████ █ ████████ ████ ████ ███████ ████ ███ ████████ █████████ ███ -███ ███████████ ███████ ███ ████ ████ ████ █████ ████ ███ ████ ███████████ ███████████ -█ ████████████ ██████████ ██████ ████ ████ ███ ████ ███ ████ ███████ █████████ - █████████████ ███████ ████████ ████ - ███████ ███ ███ ██ ██████ ████ - ███ ██████████ ████ - █████████ █ - ████ + ████████████ ██ + ███████████████████ ███ + ████████████ ██████████ ████ +█████████████ ███ ███████ ██████ ████ ███ +███████ █ ██████████████████ █ ████ ███ +█████ ████████████████ ██████ ██ ████ ███ +███ ███████████████ ███████████ ███████████████ ████████████████████ ████████ ██████████ +██████████████ █ ███████ ████ █████ █████████ █████████ █████ █████ █████ █████ █████ █████ +██████████ █████ █████ █████ ████ ████ ████ ████ ████ ████ ████████ █████████ ███ +██████ ██████████████ █ ████████ ████ ████ ███████ ████ ███ ████████ █████████ ███ +███ ███████████ ███████ ███ ████ ████ ████ █████ ████ ███ ████ ███████████ ███████████ +█ ████████████ ██████████ ██████ ████ ████ ███ ████ ███ ████ ███████ █████████ + █████████████ ███████ ████████ ████ + ███████ ███ ███ ██ ██████ ████ + ███ ██████████ ████ + █████████ █ + ████ From 2e62af2d645e52a61119dc70ec5752c4c14cfb91 Mon Sep 17 00:00:00 2001 From: Bilal Itani Date: Sun, 22 Feb 2026 23:05:51 -0600 Subject: [PATCH 10/10] Fix deploy to work over rsync, unify build and deploy steps for dev and deploy commands --- Cargo.toml | 2 +- Makefile | 13 +- cli/deploy/deploy_command.rs | 4 + cli/deploy/tests/test_deploy_default_rsync.rs | 53 +++++++- cli/deploy/tests/test_deploy_force_scp.rs | 3 +- .../test_deploy_transport_default_rsync.rs | 52 +++++++- .../test_deploy_transport_fallback_scp.rs | 34 ++++- .../test_deploy_transport_rsync_explicit.rs | 4 +- .../test_deploy_transport_scp_explicit.rs | 35 +++++- .../test_transport_default_rsync_available.rs | 52 +++++++- .../test_transport_default_scp_fallback.rs | 34 ++++- .../tests/test_default_server_dev_local.rs | 2 - ...test_default_server_dev_validates_local.rs | 2 - .../test_dev_default_server_from_config.rs | 2 - cli/dev/tests/test_dev_full_pipeline.rs | 2 - .../tests/test_dev_no_default_server_error.rs | 2 - cli/dev/tests/test_dev_oneshot_flow.rs | 2 - cli/dev/tests/test_dev_override_restart.rs | 2 - ..._dev_restart_command_from_server_config.rs | 2 - cli/dev/tests/test_dev_runs_link.rs | 6 +- cli/dev/tests/test_dev_runs_restart.rs | 2 - cli/dev/tests/test_dev_watch_mode_init.rs | 2 - cli/dev/tests/test_dev_with_default_server.rs | 6 +- cli/dev/tests/test_dev_with_server_name.rs | 2 - .../tests/test_error_invalid_server_config.rs | 2 - cli/dev/tests/test_error_link_fails.rs | 2 - .../tests/test_error_missing_restart_cmd.rs | 2 - .../test_error_missing_server_no_default.rs | 2 - cli/dev/tests/test_error_restart_fails.rs | 2 - cli/dev/tests/test_error_server_is_remote.rs | 2 - cli/dev/tests/test_error_server_not_found.rs | 6 +- cli/server/server_command.rs | 7 +- cli/tests/server_integration.rs | 83 ++++++++++++- cli/tests/src/main.rs | 2 + core/config/src/server.rs | 39 ++++-- .../test_config_merge_cli_overrides_repo.rs | 4 +- .../tests/test_default_server_resolution.rs | 4 +- .../tests/test_server_yaml_parse_ssh.rs | 4 +- core/path/Cargo.toml | 1 + core/path/src/lib.rs | 103 ++++++++++++++- features/deploy/Cargo.toml | 1 + features/deploy/src/lib.rs | 117 ++++++++++++------ features/dev/Cargo.toml | 4 +- features/dev/src/cmd/mod.rs | 101 ++++----------- features/server/src/args/default_args.rs | 1 + features/server/src/cmd/add.rs | 89 +++++++++---- features/server/src/cmd/default.rs | 35 +++++- features/server/src/cmd/list.rs | 10 +- 48 files changed, 719 insertions(+), 224 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0054009..d05a8ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hymod" -version = "0.2.8" +version = "0.3.6" edition = "2021" [[bin]] diff --git a/Makefile b/Makefile index 70ccf5c..38815f8 100644 --- a/Makefile +++ b/Makefile @@ -2,9 +2,11 @@ SHELL := /bin/bash BIN ?= hymod RUN := cargo run -q --bin $(BIN) -- +BUILD_CMD := cargo build -q --bin $(BIN) +BIN_PATH := ./target/debug/$(BIN) ARGS ?= -.PHONY: make-help run noargs help new build link dev deploy server config +.PHONY: make-help run run-built noargs help new build link dev deploy server config install-local make-help: @echo "Hymod CLI Make targets" @@ -16,6 +18,7 @@ make-help: @echo "" @echo "Targets:" @echo " run - run arbitrary args, e.g. make run ARGS='server list'" + @echo " run-built - build then run compiled binary, e.g. make run-built ARGS='server list'" @echo " noargs - run hymod with no args (logo + help)" @echo " help - run hymod --help" @echo " new - run hymod new " @@ -25,10 +28,15 @@ make-help: @echo " deploy - run hymod deploy " @echo " server - run hymod server " @echo " config - run hymod config " + @echo " install-local - build hymod and install to /usr/local/bin (uses sudo)" run: $(RUN) $(ARGS) +run-built: + $(BUILD_CMD) + $(BIN_PATH) $(ARGS) + noargs: $(RUN) @@ -55,3 +63,6 @@ server: config: $(RUN) config $(ARGS) + +install-local: + ./scripts/local_deploy.sh diff --git a/cli/deploy/deploy_command.rs b/cli/deploy/deploy_command.rs index 74165a9..086ff82 100644 --- a/cli/deploy/deploy_command.rs +++ b/cli/deploy/deploy_command.rs @@ -5,6 +5,9 @@ pub struct DeployCommand { pub server_name: Option, #[arg(long)] pub transport: Option, + /// Restart server after upload (disabled by default) + #[arg(long)] + pub restart: bool, #[arg(long)] pub dry_run: bool, /// Path to the mod directory (default: current directory) @@ -21,6 +24,7 @@ impl CliCommand for DeployCommand { let args = features_deploy::DeployArgs { server_name: self.server_name.clone(), transport: self.transport.clone(), + restart: self.restart, dry_run: self.dry_run, path: self.path.clone(), }; diff --git a/cli/deploy/tests/test_deploy_default_rsync.rs b/cli/deploy/tests/test_deploy_default_rsync.rs index cdcc1cf..601fbbf 100644 --- a/cli/deploy/tests/test_deploy_default_rsync.rs +++ b/cli/deploy/tests/test_deploy_default_rsync.rs @@ -1,4 +1,55 @@ +use assert_cmd::Command; +use predicates::prelude::*; +use std::fs; +use tempfile::TempDir; + +#[cfg(unix)] +fn make_fake_rsync(bin_dir: &std::path::Path) { + use std::os::unix::fs::PermissionsExt; + + let rsync_path = bin_dir.join("rsync"); + fs::write(&rsync_path, "#!/usr/bin/env sh\nexit 0\n").expect("failed to write fake rsync"); + let mut perms = fs::metadata(&rsync_path) + .expect("failed to stat fake rsync") + .permissions(); + perms.set_mode(0o755); + fs::set_permissions(&rsync_path, perms).expect("failed to chmod fake rsync"); +} + +#[cfg(unix)] #[test] fn test_deploy_default_rsync() { - panic!("Plan must contain UploadRsync(cache -> user@host:/mods/mod.zip) and SshRun(systemctl restart)."); + let temp_dir = TempDir::new().expect("failed to create temp dir"); + let home_dir = TempDir::new().expect("failed to create home dir"); + let bin_dir = TempDir::new().expect("failed to create bin dir"); + + #[cfg(unix)] + make_fake_rsync(bin_dir.path()); + + Command::cargo_bin("hymod") + .expect("binary not found") + .current_dir(temp_dir.path()) + .env("HOME", home_dir.path()) + .args(["new", "testmod"]) + .assert() + .success(); + + let project_dir = temp_dir.path().join("testmod"); + + Command::cargo_bin("hymod") + .expect("binary not found") + .env("HOME", home_dir.path()) + .args(["server", "add", "remote", "prod", "user@host:/path"]) + .assert() + .success(); + + Command::cargo_bin("hymod") + .expect("binary not found") + .current_dir(&project_dir) + .env("HOME", home_dir.path()) + .env("PATH", bin_dir.path()) + .args(["deploy", "prod", "--dry-run"]) + .assert() + .success() + .stdout(predicate::str::contains("UPLOAD(RSYNC)")); } diff --git a/cli/deploy/tests/test_deploy_force_scp.rs b/cli/deploy/tests/test_deploy_force_scp.rs index 35f4355..61b3649 100644 --- a/cli/deploy/tests/test_deploy_force_scp.rs +++ b/cli/deploy/tests/test_deploy_force_scp.rs @@ -26,11 +26,12 @@ fn test_deploy_force_scp() { .assert() .success(); - // Run deploy (default transport should be SCP) + // Run deploy with no rsync in PATH: default should fall back to SCP. Command::cargo_bin("hymod") .expect("binary not found") .current_dir(&project_dir) .env("HOME", home_dir.path()) + .env("PATH", "") .args(["deploy", "prod", "--dry-run"]) .assert() .success() diff --git a/cli/deploy/tests/test_deploy_transport_default_rsync.rs b/cli/deploy/tests/test_deploy_transport_default_rsync.rs index 9a86a77..810f3c4 100644 --- a/cli/deploy/tests/test_deploy_transport_default_rsync.rs +++ b/cli/deploy/tests/test_deploy_transport_default_rsync.rs @@ -1,4 +1,54 @@ +use assert_cmd::Command; +use predicates::prelude::*; +use std::fs; +use tempfile::TempDir; + +#[cfg(unix)] +fn make_fake_rsync(bin_dir: &std::path::Path) { + use std::os::unix::fs::PermissionsExt; + + let rsync_path = bin_dir.join("rsync"); + fs::write(&rsync_path, "#!/usr/bin/env sh\nexit 0\n").expect("failed to write fake rsync"); + let mut perms = fs::metadata(&rsync_path) + .expect("failed to stat fake rsync") + .permissions(); + perms.set_mode(0o755); + fs::set_permissions(&rsync_path, perms).expect("failed to chmod fake rsync"); +} + +#[cfg(unix)] #[test] fn test_deploy_transport_default_rsync() { - panic!("Execute: hymod deploy --server prod (rsync in PATH), verify rsync used by default."); + let temp_dir = TempDir::new().expect("failed to create temp dir"); + let home_dir = TempDir::new().expect("failed to create home dir"); + let bin_dir = TempDir::new().expect("failed to create bin dir"); + + make_fake_rsync(bin_dir.path()); + + Command::cargo_bin("hymod") + .expect("binary not found") + .current_dir(temp_dir.path()) + .env("HOME", home_dir.path()) + .args(["new", "testmod"]) + .assert() + .success(); + + let project_dir = temp_dir.path().join("testmod"); + + Command::cargo_bin("hymod") + .expect("binary not found") + .env("HOME", home_dir.path()) + .args(["server", "add", "remote", "prod", "user@host:/path"]) + .assert() + .success(); + + Command::cargo_bin("hymod") + .expect("binary not found") + .current_dir(&project_dir) + .env("HOME", home_dir.path()) + .env("PATH", bin_dir.path()) + .args(["deploy", "prod", "--dry-run"]) + .assert() + .success() + .stdout(predicate::str::contains("UPLOAD(RSYNC)")); } diff --git a/cli/deploy/tests/test_deploy_transport_fallback_scp.rs b/cli/deploy/tests/test_deploy_transport_fallback_scp.rs index 6fb3762..5780890 100644 --- a/cli/deploy/tests/test_deploy_transport_fallback_scp.rs +++ b/cli/deploy/tests/test_deploy_transport_fallback_scp.rs @@ -1,4 +1,36 @@ +use assert_cmd::Command; +use predicates::prelude::*; +use tempfile::TempDir; + #[test] fn test_deploy_transport_fallback_scp() { - panic!("Execute: hymod deploy --server prod (no rsync), verify fallback to scp."); + let temp_dir = TempDir::new().expect("failed to create temp dir"); + let home_dir = TempDir::new().expect("failed to create home dir"); + + Command::cargo_bin("hymod") + .expect("binary not found") + .current_dir(temp_dir.path()) + .env("HOME", home_dir.path()) + .args(["new", "testmod"]) + .assert() + .success(); + + let project_dir = temp_dir.path().join("testmod"); + + Command::cargo_bin("hymod") + .expect("binary not found") + .env("HOME", home_dir.path()) + .args(["server", "add", "remote", "prod", "user@host:/path"]) + .assert() + .success(); + + Command::cargo_bin("hymod") + .expect("binary not found") + .current_dir(&project_dir) + .env("HOME", home_dir.path()) + .env("PATH", "") + .args(["deploy", "prod", "--dry-run"]) + .assert() + .success() + .stdout(predicate::str::contains("UPLOAD(SCP)")); } diff --git a/cli/deploy/tests/test_deploy_transport_rsync_explicit.rs b/cli/deploy/tests/test_deploy_transport_rsync_explicit.rs index 9c8b8f6..9fb30bc 100644 --- a/cli/deploy/tests/test_deploy_transport_rsync_explicit.rs +++ b/cli/deploy/tests/test_deploy_transport_rsync_explicit.rs @@ -31,8 +31,10 @@ fn test_deploy_transport_rsync_explicit() { .expect("binary not found") .current_dir(&project_dir) .env("HOME", home_dir.path()) + .env("PATH", "") .args(["deploy", "prod", "--transport", "rsync", "--dry-run"]) .assert() .success() - .stdout(predicate::str::contains("UPLOAD(RSYNC)")); + .stdout(predicate::str::contains("UPLOAD(RSYNC)")) + .stdout(predicate::str::contains("UPLOAD(SCP)").not()); } diff --git a/cli/deploy/tests/test_deploy_transport_scp_explicit.rs b/cli/deploy/tests/test_deploy_transport_scp_explicit.rs index c6699c6..060ca1e 100644 --- a/cli/deploy/tests/test_deploy_transport_scp_explicit.rs +++ b/cli/deploy/tests/test_deploy_transport_scp_explicit.rs @@ -1,4 +1,37 @@ +use assert_cmd::Command; +use predicates::prelude::*; +use tempfile::TempDir; + #[test] fn test_deploy_transport_scp_explicit() { - panic!("Execute: hymod deploy --server prod --transport scp, verify scp used for upload."); + let temp_dir = TempDir::new().expect("failed to create temp dir"); + let home_dir = TempDir::new().expect("failed to create home dir"); + + Command::cargo_bin("hymod") + .expect("binary not found") + .current_dir(temp_dir.path()) + .env("HOME", home_dir.path()) + .args(["new", "testmod"]) + .assert() + .success(); + + let project_dir = temp_dir.path().join("testmod"); + + Command::cargo_bin("hymod") + .expect("binary not found") + .env("HOME", home_dir.path()) + .args(["server", "add", "remote", "prod", "user@host:/path"]) + .assert() + .success(); + + Command::cargo_bin("hymod") + .expect("binary not found") + .current_dir(&project_dir) + .env("HOME", home_dir.path()) + .env("PATH", "") + .args(["deploy", "prod", "--transport", "scp", "--dry-run"]) + .assert() + .success() + .stdout(predicate::str::contains("UPLOAD(SCP)")) + .stdout(predicate::str::contains("UPLOAD(RSYNC)").not()); } diff --git a/cli/deploy/tests/test_transport_default_rsync_available.rs b/cli/deploy/tests/test_transport_default_rsync_available.rs index 68a1961..cddf9c2 100644 --- a/cli/deploy/tests/test_transport_default_rsync_available.rs +++ b/cli/deploy/tests/test_transport_default_rsync_available.rs @@ -1,4 +1,54 @@ +use assert_cmd::Command; +use predicates::prelude::*; +use std::fs; +use tempfile::TempDir; + +#[cfg(unix)] +fn make_fake_rsync(bin_dir: &std::path::Path) { + use std::os::unix::fs::PermissionsExt; + + let rsync_path = bin_dir.join("rsync"); + fs::write(&rsync_path, "#!/usr/bin/env sh\nexit 0\n").expect("failed to write fake rsync"); + let mut perms = fs::metadata(&rsync_path) + .expect("failed to stat fake rsync") + .permissions(); + perms.set_mode(0o755); + fs::set_permissions(&rsync_path, perms).expect("failed to chmod fake rsync"); +} + +#[cfg(unix)] #[test] fn test_transport_default_rsync_available() { - panic!("Execute: hymod deploy --server prod (rsync in PATH), verify rsync used by default."); + let temp_dir = TempDir::new().expect("failed to create temp dir"); + let home_dir = TempDir::new().expect("failed to create home dir"); + let bin_dir = TempDir::new().expect("failed to create bin dir"); + + make_fake_rsync(bin_dir.path()); + + Command::cargo_bin("hymod") + .expect("binary not found") + .current_dir(temp_dir.path()) + .env("HOME", home_dir.path()) + .args(["new", "testmod"]) + .assert() + .success(); + + let project_dir = temp_dir.path().join("testmod"); + + Command::cargo_bin("hymod") + .expect("binary not found") + .env("HOME", home_dir.path()) + .args(["server", "add", "remote", "prod", "user@host:/path"]) + .assert() + .success(); + + Command::cargo_bin("hymod") + .expect("binary not found") + .current_dir(&project_dir) + .env("HOME", home_dir.path()) + .env("PATH", bin_dir.path()) + .args(["deploy", "prod", "--dry-run"]) + .assert() + .success() + .stdout(predicate::str::contains("UPLOAD(RSYNC)")); } diff --git a/cli/deploy/tests/test_transport_default_scp_fallback.rs b/cli/deploy/tests/test_transport_default_scp_fallback.rs index 620f6cb..a49df8f 100644 --- a/cli/deploy/tests/test_transport_default_scp_fallback.rs +++ b/cli/deploy/tests/test_transport_default_scp_fallback.rs @@ -1,4 +1,36 @@ +use assert_cmd::Command; +use predicates::prelude::*; +use tempfile::TempDir; + #[test] fn test_transport_default_scp_fallback() { - panic!("Execute: hymod deploy --server prod (no rsync in PATH), verify fallback to scp."); + let temp_dir = TempDir::new().expect("failed to create temp dir"); + let home_dir = TempDir::new().expect("failed to create home dir"); + + Command::cargo_bin("hymod") + .expect("binary not found") + .current_dir(temp_dir.path()) + .env("HOME", home_dir.path()) + .args(["new", "testmod"]) + .assert() + .success(); + + let project_dir = temp_dir.path().join("testmod"); + + Command::cargo_bin("hymod") + .expect("binary not found") + .env("HOME", home_dir.path()) + .args(["server", "add", "remote", "prod", "user@host:/path"]) + .assert() + .success(); + + Command::cargo_bin("hymod") + .expect("binary not found") + .current_dir(&project_dir) + .env("HOME", home_dir.path()) + .env("PATH", "") + .args(["deploy", "prod", "--dry-run"]) + .assert() + .success() + .stdout(predicate::str::contains("UPLOAD(SCP)")); } diff --git a/cli/dev/tests/test_default_server_dev_local.rs b/cli/dev/tests/test_default_server_dev_local.rs index 0f0cc43..fb10d3e 100644 --- a/cli/dev/tests/test_default_server_dev_local.rs +++ b/cli/dev/tests/test_default_server_dev_local.rs @@ -1,5 +1,3 @@ - - #[test] fn test_default_server_dev_local() { println!("Execute: hymod dev (with default_server=local-dev in hymod.yaml), verify dev uses default local server."); diff --git a/cli/dev/tests/test_default_server_dev_validates_local.rs b/cli/dev/tests/test_default_server_dev_validates_local.rs index 3b95ab5..21ea4bb 100644 --- a/cli/dev/tests/test_default_server_dev_validates_local.rs +++ b/cli/dev/tests/test_default_server_dev_validates_local.rs @@ -1,5 +1,3 @@ - - #[test] fn test_default_server_dev_validates_local() { println!("Execute: hymod dev (with default_server=prod kind=ssh), verify error 'Cannot run dev against remote server'."); diff --git a/cli/dev/tests/test_dev_default_server_from_config.rs b/cli/dev/tests/test_dev_default_server_from_config.rs index 9957d37..b0e4791 100644 --- a/cli/dev/tests/test_dev_default_server_from_config.rs +++ b/cli/dev/tests/test_dev_default_server_from_config.rs @@ -1,5 +1,3 @@ - - #[test] fn test_dev_default_server_from_config() { println!("Must use hymod.yaml defaults.server when --server flag is omitted."); diff --git a/cli/dev/tests/test_dev_full_pipeline.rs b/cli/dev/tests/test_dev_full_pipeline.rs index 1973fdc..d5d3a56 100644 --- a/cli/dev/tests/test_dev_full_pipeline.rs +++ b/cli/dev/tests/test_dev_full_pipeline.rs @@ -1,5 +1,3 @@ - - #[test] fn test_dev_full_pipeline() { println!( diff --git a/cli/dev/tests/test_dev_no_default_server_error.rs b/cli/dev/tests/test_dev_no_default_server_error.rs index f00ec5e..cfe30fb 100644 --- a/cli/dev/tests/test_dev_no_default_server_error.rs +++ b/cli/dev/tests/test_dev_no_default_server_error.rs @@ -1,5 +1,3 @@ - - #[test] fn test_dev_no_default_server_error() { println!("Must return Error: 'No default server configured, please specify --server ' when no default."); diff --git a/cli/dev/tests/test_dev_oneshot_flow.rs b/cli/dev/tests/test_dev_oneshot_flow.rs index c847319..f0cd81e 100644 --- a/cli/dev/tests/test_dev_oneshot_flow.rs +++ b/cli/dev/tests/test_dev_oneshot_flow.rs @@ -1,5 +1,3 @@ - - #[test] fn test_dev_oneshot_flow() { println!("Plan must be composition of: Build Plan + Link Plan + RunProcess(restart)."); diff --git a/cli/dev/tests/test_dev_override_restart.rs b/cli/dev/tests/test_dev_override_restart.rs index c65f548..62a2ea7 100644 --- a/cli/dev/tests/test_dev_override_restart.rs +++ b/cli/dev/tests/test_dev_override_restart.rs @@ -1,5 +1,3 @@ - - #[test] fn test_dev_override_restart() { println!("Plan must end with RunProcess(echo restart)."); diff --git a/cli/dev/tests/test_dev_restart_command_from_server_config.rs b/cli/dev/tests/test_dev_restart_command_from_server_config.rs index c0badb4..f540bde 100644 --- a/cli/dev/tests/test_dev_restart_command_from_server_config.rs +++ b/cli/dev/tests/test_dev_restart_command_from_server_config.rs @@ -1,5 +1,3 @@ - - #[test] fn test_dev_restart_command_from_server_config() { println!("Plan must use server.restart.cmd from server config file."); diff --git a/cli/dev/tests/test_dev_runs_link.rs b/cli/dev/tests/test_dev_runs_link.rs index 5b646f1..8a0a9f8 100644 --- a/cli/dev/tests/test_dev_runs_link.rs +++ b/cli/dev/tests/test_dev_runs_link.rs @@ -1,6 +1,6 @@ - - #[test] fn test_dev_runs_link() { - println!("Execute: hymod dev --server local-dev, verify link step executed and symlink created."); + println!( + "Execute: hymod dev --server local-dev, verify link step executed and symlink created." + ); } diff --git a/cli/dev/tests/test_dev_runs_restart.rs b/cli/dev/tests/test_dev_runs_restart.rs index dad06c5..99aae1f 100644 --- a/cli/dev/tests/test_dev_runs_restart.rs +++ b/cli/dev/tests/test_dev_runs_restart.rs @@ -1,5 +1,3 @@ - - #[test] fn test_dev_runs_restart() { println!("Execute: hymod dev --server local-dev, verify restart command executed."); diff --git a/cli/dev/tests/test_dev_watch_mode_init.rs b/cli/dev/tests/test_dev_watch_mode_init.rs index 8299d7c..0a80ffd 100644 --- a/cli/dev/tests/test_dev_watch_mode_init.rs +++ b/cli/dev/tests/test_dev_watch_mode_init.rs @@ -1,5 +1,3 @@ - - #[test] fn test_dev_watch_mode_init() { println!("Must verify file watcher is registered on ./src."); diff --git a/cli/dev/tests/test_dev_with_default_server.rs b/cli/dev/tests/test_dev_with_default_server.rs index 747b1bc..89510f6 100644 --- a/cli/dev/tests/test_dev_with_default_server.rs +++ b/cli/dev/tests/test_dev_with_default_server.rs @@ -1,6 +1,6 @@ - - #[test] fn test_dev_with_default_server() { - println!("Execute: hymod dev (with default_server in hymod.yaml), verify dev loop uses default."); + println!( + "Execute: hymod dev (with default_server in hymod.yaml), verify dev loop uses default." + ); } diff --git a/cli/dev/tests/test_dev_with_server_name.rs b/cli/dev/tests/test_dev_with_server_name.rs index cd439a9..0b98cb3 100644 --- a/cli/dev/tests/test_dev_with_server_name.rs +++ b/cli/dev/tests/test_dev_with_server_name.rs @@ -1,5 +1,3 @@ - - #[test] fn test_dev_with_server_name() { println!("Execute: hymod dev --server local-dev, verify build → link → restart executed."); diff --git a/cli/dev/tests/test_error_invalid_server_config.rs b/cli/dev/tests/test_error_invalid_server_config.rs index 954df0b..3794575 100644 --- a/cli/dev/tests/test_error_invalid_server_config.rs +++ b/cli/dev/tests/test_error_invalid_server_config.rs @@ -1,5 +1,3 @@ - - #[test] fn test_error_invalid_server_config() { println!( diff --git a/cli/dev/tests/test_error_link_fails.rs b/cli/dev/tests/test_error_link_fails.rs index 168b56e..1f32fc6 100644 --- a/cli/dev/tests/test_error_link_fails.rs +++ b/cli/dev/tests/test_error_link_fails.rs @@ -1,5 +1,3 @@ - - #[test] fn test_error_link_fails() { println!("Execute: hymod dev --server local-dev (with link error), verify error 'Dev failed: link error'."); diff --git a/cli/dev/tests/test_error_missing_restart_cmd.rs b/cli/dev/tests/test_error_missing_restart_cmd.rs index b75c865..dfb2c45 100644 --- a/cli/dev/tests/test_error_missing_restart_cmd.rs +++ b/cli/dev/tests/test_error_missing_restart_cmd.rs @@ -1,5 +1,3 @@ - - #[test] fn test_error_missing_restart_cmd() { println!( diff --git a/cli/dev/tests/test_error_missing_server_no_default.rs b/cli/dev/tests/test_error_missing_server_no_default.rs index 1cbe245..e679d75 100644 --- a/cli/dev/tests/test_error_missing_server_no_default.rs +++ b/cli/dev/tests/test_error_missing_server_no_default.rs @@ -1,5 +1,3 @@ - - #[test] fn test_error_missing_server_no_default() { println!("Execute: hymod dev (no --server, no default), verify error 'No server specified and no default configured'."); diff --git a/cli/dev/tests/test_error_restart_fails.rs b/cli/dev/tests/test_error_restart_fails.rs index d237971..c30061f 100644 --- a/cli/dev/tests/test_error_restart_fails.rs +++ b/cli/dev/tests/test_error_restart_fails.rs @@ -1,5 +1,3 @@ - - #[test] fn test_error_restart_fails() { println!("Execute: hymod dev --server local-dev (with bad restart cmd), verify error 'Dev failed: restart error'."); diff --git a/cli/dev/tests/test_error_server_is_remote.rs b/cli/dev/tests/test_error_server_is_remote.rs index e9e5997..07c6435 100644 --- a/cli/dev/tests/test_error_server_is_remote.rs +++ b/cli/dev/tests/test_error_server_is_remote.rs @@ -1,5 +1,3 @@ - - #[test] fn test_error_server_is_remote() { println!("Execute: hymod dev --server prod-ssh, verify error 'Cannot run dev against remote server'."); diff --git a/cli/dev/tests/test_error_server_not_found.rs b/cli/dev/tests/test_error_server_not_found.rs index 0957000..c55923e 100644 --- a/cli/dev/tests/test_error_server_not_found.rs +++ b/cli/dev/tests/test_error_server_not_found.rs @@ -1,6 +1,6 @@ - - #[test] fn test_error_server_not_found() { - println!("Execute: hymod dev --server nonexistent, verify error 'Server nonexistent not found'."); + println!( + "Execute: hymod dev --server nonexistent, verify error 'Server nonexistent not found'." + ); } diff --git a/cli/server/server_command.rs b/cli/server/server_command.rs index 9f2c07f..b5d66ad 100644 --- a/cli/server/server_command.rs +++ b/cli/server/server_command.rs @@ -69,8 +69,11 @@ impl CliCommand for ServerCommand { server_root: None, }, ), - ServerCommand::Default { kind: _, name } => features_server::ServerCommand::Default( - features_server::args::default_args::ServerDefaultArgs { name: name.clone() }, + ServerCommand::Default { kind, name } => features_server::ServerCommand::Default( + features_server::args::default_args::ServerDefaultArgs { + kind: kind.clone(), + name: name.clone(), + }, ), ServerCommand::Remove { name } => features_server::ServerCommand::Remove( features_server::args::remove_args::ServerRemoveArgs { name: name.clone() }, diff --git a/cli/tests/server_integration.rs b/cli/tests/server_integration.rs index 0839d9c..f12f61f 100644 --- a/cli/tests/server_integration.rs +++ b/cli/tests/server_integration.rs @@ -58,6 +58,40 @@ fn test_server_add_remote() { assert!(content.contains("host: example.com")); } +#[test] +fn test_server_add_remote_with_path_parses_server_root() { + let temp_home = TempDir::new().expect("failed to create temp dir"); + + Command::cargo_bin("hymod") + .expect("binary not found") + .env("HOME", temp_home.path()) + .args([ + "server", + "add", + "remote", + "hymodtest", + "root@170.205.24.203:/root/playground/hymodtest", + ]) + .assert() + .success() + .stdout(predicate::str::contains( + "Server 'hymodtest' added successfully", + )); + + let config_path = temp_home + .path() + .join(".hymod") + .join("servers.d") + .join("hymodtest.yaml"); + assert!(config_path.exists()); + + let content = fs::read_to_string(config_path).unwrap(); + assert!(content.contains("kind: remote")); + assert!(content.contains("user: root")); + assert!(content.contains("host: 170.205.24.203")); + assert!(content.contains("server_root: /root/playground/hymodtest")); +} + #[test] fn test_server_already_exists() { let temp_home = TempDir::new().expect("failed to create temp dir"); @@ -126,7 +160,7 @@ fn test_server_default() { .args(["server", "default", "local", "s1"]) .assert() .success() - .stdout(predicate::str::contains("Default server set to 's1'")); + .stdout(predicate::str::contains("Default local server set to 's1'")); // Check list highlight Command::cargo_bin("hymod") @@ -138,6 +172,53 @@ fn test_server_default() { .stdout(predicate::str::contains("* s1 [local]")); } +#[test] +fn test_server_default_separate_local_and_remote() { + let temp_home = TempDir::new().expect("failed to create temp dir"); + + Command::cargo_bin("hymod") + .expect("binary not found") + .env("HOME", temp_home.path()) + .args(["server", "add", "local", "local1", "/tmp/local1"]) + .assert() + .success(); + + Command::cargo_bin("hymod") + .expect("binary not found") + .env("HOME", temp_home.path()) + .args(["server", "add", "remote", "remote1", "user@example.com"]) + .assert() + .success(); + + Command::cargo_bin("hymod") + .expect("binary not found") + .env("HOME", temp_home.path()) + .args(["server", "default", "local", "local1"]) + .assert() + .success(); + + Command::cargo_bin("hymod") + .expect("binary not found") + .env("HOME", temp_home.path()) + .args(["server", "default", "remote", "remote1"]) + .assert() + .success(); + + let default_local = temp_home + .path() + .join(".hymod") + .join("servers.d") + .join("default.local"); + let default_remote = temp_home + .path() + .join(".hymod") + .join("servers.d") + .join("default.remote"); + + assert_eq!(fs::read_to_string(default_local).unwrap().trim(), "local1"); + assert_eq!(fs::read_to_string(default_remote).unwrap().trim(), "remote1"); +} + #[test] fn test_server_remove() { let temp_home = TempDir::new().expect("failed to create temp dir"); diff --git a/cli/tests/src/main.rs b/cli/tests/src/main.rs index 14bcc98..8ae9c20 100644 --- a/cli/tests/src/main.rs +++ b/cli/tests/src/main.rs @@ -144,7 +144,9 @@ fn main() { let args = features_deploy::DeployArgs { server_name, transport, + restart: false, dry_run, + path: None, }; let _plan = features_deploy::generate_plan(args); // TODO: Pass plan to core::ops::execute() diff --git a/core/config/src/server.rs b/core/config/src/server.rs index 41e08cd..fb977f5 100644 --- a/core/config/src/server.rs +++ b/core/config/src/server.rs @@ -137,21 +137,46 @@ pub fn list_servers() -> Result, String> { } pub fn get_default_server() -> Result, String> { + get_default_server_for_kind(&ServerKind::Remote) +} + +pub fn set_default_server(name: &str) -> Result<(), String> { + set_default_server_for_kind(&ServerKind::Remote, name) +} + +pub fn get_default_server_for_kind(kind: &ServerKind) -> Result, String> { let dir = get_server_config_dir()?; - let path = dir.join("default"); - if !path.exists() { - return Ok(None); + let path = dir.join(default_filename_for_kind(kind)); + if path.exists() { + let content = fs::read_to_string(path).map_err(|e| e.to_string())?; + return Ok(Some(content.trim().to_string())); } - let content = fs::read_to_string(path).map_err(|e| e.to_string())?; - Ok(Some(content.trim().to_string())) + + // Backward compatibility: if only legacy default exists, use it as remote default. + if *kind == ServerKind::Remote { + let legacy_path = dir.join("default"); + if legacy_path.exists() { + let content = fs::read_to_string(legacy_path).map_err(|e| e.to_string())?; + return Ok(Some(content.trim().to_string())); + } + } + + Ok(None) } -pub fn set_default_server(name: &str) -> Result<(), String> { +pub fn set_default_server_for_kind(kind: &ServerKind, name: &str) -> Result<(), String> { let dir = get_server_config_dir()?; if !dir.exists() { fs::create_dir_all(&dir).map_err(|e| e.to_string())?; } - let path = dir.join("default"); + let path = dir.join(default_filename_for_kind(kind)); fs::write(path, name).map_err(|e| e.to_string())?; Ok(()) } + +fn default_filename_for_kind(kind: &ServerKind) -> &'static str { + match kind { + ServerKind::Local => "default.local", + ServerKind::Remote => "default.remote", + } +} diff --git a/core/config/tests/test_config_merge_cli_overrides_repo.rs b/core/config/tests/test_config_merge_cli_overrides_repo.rs index f9e6ada..588534a 100644 --- a/core/config/tests/test_config_merge_cli_overrides_repo.rs +++ b/core/config/tests/test_config_merge_cli_overrides_repo.rs @@ -1,4 +1,6 @@ #[test] fn test_config_merge_cli_overrides_repo() { - panic!("Must use CLI --server flag value when both CLI and hymod.yaml defaults.server are set."); + panic!( + "Must use CLI --server flag value when both CLI and hymod.yaml defaults.server are set." + ); } diff --git a/core/config/tests/test_default_server_resolution.rs b/core/config/tests/test_default_server_resolution.rs index 010e8b0..a01cabe 100644 --- a/core/config/tests/test_default_server_resolution.rs +++ b/core/config/tests/test_default_server_resolution.rs @@ -1,4 +1,6 @@ #[test] fn test_default_server_resolution() { - panic!("Must resolve default server from hymod.yaml defaults.server when --server flag omitted."); + panic!( + "Must resolve default server from hymod.yaml defaults.server when --server flag omitted." + ); } diff --git a/core/config/tests/test_server_yaml_parse_ssh.rs b/core/config/tests/test_server_yaml_parse_ssh.rs index 0f25393..1b4d80b 100644 --- a/core/config/tests/test_server_yaml_parse_ssh.rs +++ b/core/config/tests/test_server_yaml_parse_ssh.rs @@ -1,4 +1,6 @@ #[test] fn test_server_yaml_parse_ssh() { - panic!("Must parse SSH server config with kind=ssh, ssh.host, ssh.user, ssh.port, identity_file."); + panic!( + "Must parse SSH server config with kind=ssh, ssh.host, ssh.user, ssh.port, identity_file." + ); } diff --git a/core/path/Cargo.toml b/core/path/Cargo.toml index f9ea711..b043b4c 100644 --- a/core/path/Cargo.toml +++ b/core/path/Cargo.toml @@ -4,3 +4,4 @@ version = "0.1.0" edition = "2021" [dependencies] +serde_json = "1.0" diff --git a/core/path/src/lib.rs b/core/path/src/lib.rs index 035fbff..8b8cda3 100644 --- a/core/path/src/lib.rs +++ b/core/path/src/lib.rs @@ -1 +1,102 @@ -// Placeholder for path utilities +use std::fs; +use std::path::{Path, PathBuf}; + +pub struct ResolvedArtifact { + pub source_path: PathBuf, + pub target_file_name: String, +} + +pub fn resolve_mod_artifact(mod_dir: &Path) -> ResolvedArtifact { + let build_libs = mod_dir.join("build").join("libs"); + let preferred_name = jar_name_from_manifest(mod_dir); + let guessed_name = jar_name_from_gradle_properties(mod_dir); + + let source_path = if let Some(name) = preferred_name.as_ref() { + let expected = build_libs.join(name); + if expected.is_file() { + expected + } else if let Some(candidate) = find_existing_artifact(&build_libs) { + candidate + } else { + expected + } + } else if let Some(candidate) = find_existing_artifact(&build_libs) { + candidate + } else { + build_libs.join(&guessed_name) + }; + + let target_file_name = preferred_name.unwrap_or_else(|| { + source_path + .file_name() + .map(|f| f.to_string_lossy().to_string()) + .unwrap_or(guessed_name) + }); + + ResolvedArtifact { + source_path, + target_file_name, + } +} + +fn find_existing_artifact(build_libs: &Path) -> Option { + let entries = fs::read_dir(build_libs).ok()?; + let mut candidates = Vec::new(); + + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().and_then(|ext| ext.to_str()) != Some("jar") { + continue; + } + + let file_name = path + .file_name() + .map(|f| f.to_string_lossy()) + .unwrap_or_default(); + + if file_name.contains("-sources") + || file_name.contains("-javadoc") + || file_name.contains("-plain") + { + continue; + } + + candidates.push(path); + } + + candidates.sort(); + candidates.pop() +} + +fn jar_name_from_manifest(mod_dir: &Path) -> Option { + let path = mod_dir + .join("src") + .join("main") + .join("resources") + .join("manifest.json"); + let content = fs::read_to_string(path).ok()?; + let json: serde_json::Value = serde_json::from_str(&content).ok()?; + + let name = json.get("Name").and_then(|v| v.as_str())?; + let version = json.get("Version").and_then(|v| v.as_str())?; + Some(format!("{name}-{version}.jar")) +} + +fn jar_name_from_gradle_properties(mod_dir: &Path) -> String { + let gradle_props = mod_dir.join("gradle.properties"); + if let Ok(content) = fs::read_to_string(gradle_props) { + let mut name = "mod".to_string(); + let mut version = "1.0.0".to_string(); + for line in content.lines() { + if let Some(stripped) = line.strip_prefix("mod_name=") { + name = stripped.trim().to_string(); + } + if let Some(stripped) = line.strip_prefix("mod_version=") { + version = stripped.trim().to_string(); + } + } + return format!("{name}-{version}.jar"); + } + + "mod.jar".to_string() +} diff --git a/features/deploy/Cargo.toml b/features/deploy/Cargo.toml index 64c73db..27df663 100644 --- a/features/deploy/Cargo.toml +++ b/features/deploy/Cargo.toml @@ -6,3 +6,4 @@ edition = "2021" [dependencies] core-plan = { path = "../../core/plan" } core-config = { path = "../../core/config" } +core-path = { path = "../../core/path" } diff --git a/features/deploy/src/lib.rs b/features/deploy/src/lib.rs index 5a6248e..511bafe 100644 --- a/features/deploy/src/lib.rs +++ b/features/deploy/src/lib.rs @@ -1,20 +1,26 @@ pub struct DeployArgs { pub server_name: Option, pub transport: Option, + pub restart: bool, pub dry_run: bool, pub path: Option, } -use core_config::{get_default_server, load_server_config, ServerKind}; +use core_config::{get_default_server_for_kind, load_server_config, ServerKind}; +use core_path::resolve_mod_artifact; use core_plan::{Plan, Step}; -use std::fs; +use std::path::Path; use std::path::PathBuf; pub fn generate_plan(args: DeployArgs) -> core_plan::Plan { let server_name = args .server_name - .or_else(|| get_default_server().ok().flatten()) - .expect("No server specified and no default configured"); + .or_else(|| { + get_default_server_for_kind(&ServerKind::Remote) + .ok() + .flatten() + }) + .expect("No server specified and no default remote server configured"); let config = load_server_config(&server_name).expect("Failed to load server config"); @@ -39,29 +45,9 @@ pub fn generate_plan(args: DeployArgs) -> core_plan::Plan { }); // 2. Identify Artifact - // Heuristic: Read gradle.properties to predict the name - let gradle_props = mod_dir.join("gradle.properties"); - let jar_name = if let Ok(content) = fs::read_to_string(&gradle_props) { - let mut name = "mod".to_string(); - let mut version = "1.0.0".to_string(); - for line in content.lines() { - if let Some(stripped) = line.strip_prefix("mod_name=") { - name = stripped.trim().to_string(); - } - if let Some(stripped) = line.strip_prefix("mod_version=") { - version = stripped.trim().to_string(); - } - } - format!("{}-{}.jar", name, version) - } else { - "mod.jar".to_string() - }; - - let source_path = mod_dir - .join("build/libs") - .join(&jar_name) - .to_string_lossy() - .to_string(); + let artifact = resolve_mod_artifact(&mod_dir); + let jar_name = artifact.target_file_name; + let source_path = artifact.source_path.to_string_lossy().to_string(); // 3. Deploy match config.server.kind { @@ -76,11 +62,28 @@ pub fn generate_plan(args: DeployArgs) -> core_plan::Plan { }); } ServerKind::Remote => { - let remote_dest = format!("{}/{}", config.server.mods_dir, jar_name); - if config.server.remote.is_some() { - // Determine transport - // Default to scp if not specified (or explicitly scp) - if args.transport.as_deref().unwrap_or("scp") == "rsync" { + if let Some(remote) = config.server.remote.as_ref() { + let remote_mods_dir = PathBuf::from(&config.server.server_root) + .join(&config.server.mods_dir) + .to_string_lossy() + .to_string(); + + // Ensure remote destination directory exists before upload. + steps.push(Step::SshRun { + host: remote.host.clone(), + user: remote.user.clone(), + cmd: format!("mkdir -p {}", remote_mods_dir), + }); + + let remote_file_path = PathBuf::from(&config.server.server_root) + .join(&config.server.mods_dir) + .join(&jar_name) + .to_string_lossy() + .to_string(); + let remote_dest = format!("{}@{}:{}", remote.user, remote.host, remote_file_path); + let transport = resolve_transport(args.transport.as_deref()); + + if transport == "rsync" { steps.push(Step::UploadRsync { local: source_path, remote: remote_dest, @@ -99,12 +102,50 @@ pub fn generate_plan(args: DeployArgs) -> core_plan::Plan { } } - // 4. Restart - steps.push(Step::RunProcess { - cmd: config.server.restart.cmd.clone(), - args: vec![], - cwd: None, - }); + // 4. Restart (opt-in) + if args.restart { + steps.push(Step::RunProcess { + cmd: config.server.restart.cmd.clone(), + args: vec![], + cwd: None, + }); + } Plan { steps } } + +fn resolve_transport(explicit: Option<&str>) -> &'static str { + match explicit { + Some("rsync") => "rsync", + Some("scp") => "scp", + Some(other) => panic!("Invalid transport: must be rsync or scp (got '{other}')"), + None => { + if is_command_available("rsync") { + "rsync" + } else { + "scp" + } + } + } +} + +fn is_command_available(cmd: &str) -> bool { + let Some(path_var) = std::env::var_os("PATH") else { + return false; + }; + + std::env::split_paths(&path_var).any(|dir| command_exists_in_dir(&dir, cmd)) +} + +fn command_exists_in_dir(dir: &Path, cmd: &str) -> bool { + if cfg!(windows) { + for candidate in [cmd.to_string(), format!("{cmd}.exe"), format!("{cmd}.bat")] { + if dir.join(candidate).is_file() { + return true; + } + } + false + } else { + dir.join(cmd).is_file() + } +} diff --git a/features/dev/Cargo.toml b/features/dev/Cargo.toml index 3b21213..776c16b 100644 --- a/features/dev/Cargo.toml +++ b/features/dev/Cargo.toml @@ -6,8 +6,6 @@ edition = "2021" [dependencies] anyhow = "1.0" colored = "2.0" -walkdir = "2.3" core-plan = { path = "../../core/plan" } core-config = { path = "../../core/config" } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" +core-path = { path = "../../core/path" } diff --git a/features/dev/src/cmd/mod.rs b/features/dev/src/cmd/mod.rs index d427198..5633bae 100644 --- a/features/dev/src/cmd/mod.rs +++ b/features/dev/src/cmd/mod.rs @@ -1,20 +1,10 @@ use crate::args::DevArgs; use anyhow::{bail, Context, Result}; use colored::*; -use serde::Deserialize; +use core_path::resolve_mod_artifact; use std::env; -use std::fs; use std::path::PathBuf; use std::process::Command; -use walkdir::WalkDir; - -#[derive(Deserialize)] -struct Manifest { - #[serde(rename = "Name")] - name: String, - #[serde(rename = "Version")] - version: String, -} pub fn run(args: DevArgs) -> Result<()> { // 1. Resolve mod directory @@ -75,39 +65,14 @@ pub fn run(args: DevArgs) -> Result<()> { } // 3. Identify artifact - // Look for jars in build/libs/ - excluding sources/javadoc jars if possible, but for simplicity pick the first likely candidate or all of them. - // The requirement says "ONLY THE CORRECT ONE, NOT INCLUDED LIBRARIES". - // Usually build/libs contains: mod-version.jar, mod-version-sources.jar, etc. - // We should pick the main one. Typically the one that doesn't end in -sources.jar or -javadoc.jar. - - let build_libs = mod_dir.join("build").join("libs"); - if !build_libs.exists() { - bail!("Build directory not found at {}. Gradle build may have failed silently or output elsewhere.", build_libs.display()); - } - - let mut artifact_path: Option = None; - - for entry in WalkDir::new(&build_libs) - .max_depth(1) - .into_iter() - .filter_map(|e| e.ok()) - { - let path = entry.path(); - if path.extension().map_or(false, |ext| ext == "jar") { - let file_name = path.file_name().unwrap().to_string_lossy(); - if !file_name.contains("-sources") - && !file_name.contains("-javadoc") - && !file_name.contains("-plain") - { - artifact_path = Some(path.to_path_buf()); - break; - } - } + let artifact = resolve_mod_artifact(&mod_dir); + if !artifact.source_path.exists() { + bail!( + "Could not find a suitable .jar artifact in build/libs/ (expected {}).", + artifact.source_path.display() + ); } - let artifact = - artifact_path.context("Could not find a suitable .jar artifact in build/libs/")?; - // 4. Resolve Target and deploy let destination_dir = if let Some(target_str) = args.target { // Check if target_str is a valid directory path @@ -138,9 +103,12 @@ pub fn run(args: DevArgs) -> Result<()> { } } else { // Use default server - let default_server_name = core_config::get_default_server() - .map_err(|e| anyhow::anyhow!("Failed to get default server: {}", e))? - .context("No target provided and no default server configured. Use 'hymod server set-default ' or provide a target argument.")?; + let default_server_name = + core_config::get_default_server_for_kind(&core_config::ServerKind::Local) + .map_err(|e| anyhow::anyhow!("Failed to get default local server: {}", e))? + .context( + "No target provided and no default local server configured. Use 'hymod server default local ' or provide a target argument.", + )?; let server_cfg = core_config::load_server_config(&default_server_name) .map_err(|e| anyhow::anyhow!("Failed to load server config: {}", e))?; @@ -155,43 +123,16 @@ pub fn run(args: DevArgs) -> Result<()> { }; if !destination_dir.exists() { - bail!( - "Destination directory does not exist: {}", - destination_dir.display() - ); + std::fs::create_dir_all(&destination_dir).with_context(|| { + format!( + "Failed to create destination directory: {}", + destination_dir.display() + ) + })?; } // 5. Copy artifact - let file_name = artifact.file_name().context("Artifact has no file name")?; - - // Try to read manifest.json for correct naming - let manifest_path = mod_dir.join("src/main/resources/manifest.json"); - let target_name = if manifest_path.exists() { - if let Ok(content) = fs::read_to_string(&manifest_path) { - if let Ok(manifest) = serde_json::from_str::(&content) { - format!("{}-{}.jar", manifest.name, manifest.version) - } else { - println!( - "{} Failed to parse manifest.json, using build artifact name", - "!!".yellow() - ); - file_name.to_string_lossy().into_owned() - } - } else { - println!( - "{} Failed to read manifest.json, using build artifact name", - "!!".yellow() - ); - file_name.to_string_lossy().into_owned() - } - } else { - println!( - "{} No manifest.json found at {}, using build artifact name", - "!!".yellow(), - manifest_path.display() - ); - file_name.to_string_lossy().into_owned() - }; + let target_name = artifact.target_file_name; let dest_file = destination_dir.join(&target_name); @@ -201,7 +142,7 @@ pub fn run(args: DevArgs) -> Result<()> { target_name, destination_dir.display() ); - std::fs::copy(&artifact, &dest_file) + std::fs::copy(&artifact.source_path, &dest_file) .with_context(|| format!("Failed to copy artifact to {}", destination_dir.display()))?; println!( diff --git a/features/server/src/args/default_args.rs b/features/server/src/args/default_args.rs index 9c66b14..2d61f34 100644 --- a/features/server/src/args/default_args.rs +++ b/features/server/src/args/default_args.rs @@ -1,3 +1,4 @@ pub struct ServerDefaultArgs { + pub kind: String, pub name: String, } diff --git a/features/server/src/cmd/add.rs b/features/server/src/cmd/add.rs index 046060f..81e0128 100644 --- a/features/server/src/cmd/add.rs +++ b/features/server/src/cmd/add.rs @@ -22,33 +22,22 @@ pub fn run(args: ServerAddArgs) { } }; - let mut server_root = "/opt/hytale".to_string(); // Default - let mut remote_block = None; - - match kind { - ServerKind::Local => { - server_root = args.uri.clone(); - } + let (server_root, remote_block) = match kind { + ServerKind::Local => (args.uri.clone(), None), ServerKind::Remote => { - // Check if URI is user@host or just host - let parts: Vec<&str> = args.uri.split('@').collect(); - let (user, host) = if parts.len() == 2 { - (parts[0].to_string(), parts[1].to_string()) - } else { - ("root".to_string(), args.uri.clone()) // Default user root? Or current user? - }; - - remote_block = Some(RemoteBlock { - host, - user, - port: 22, - identity_file: None, - known_hosts_file: None, - }); - // server_root stays default or we could parse it from URI if standard schemes supported it (ssh://user@host/path) - // For now, prompt implies just connection info. + let (user, host, parsed_server_root) = parse_remote_uri(&args.uri); + ( + parsed_server_root, + Some(RemoteBlock { + host, + user, + port: 22, + identity_file: None, + known_hosts_file: None, + }), + ) } - } + }; let config = ServerConfig { server: ServerBlock { @@ -71,3 +60,53 @@ pub fn run(args: ServerAddArgs) { println!("Server '{}' added successfully.", args.name); } + +fn parse_remote_uri(uri: &str) -> (String, String, String) { + let default_root = "/opt/hytale".to_string(); + + let (user, host_and_maybe_path) = match uri.split_once('@') { + Some((u, rest)) if !u.is_empty() => (u.to_string(), rest), + _ => ("root".to_string(), uri), + }; + + let (host, server_root) = match host_and_maybe_path.split_once(":/") { + Some((h, root_tail)) if !h.is_empty() => (h.to_string(), format!("/{}", root_tail)), + _ => (host_and_maybe_path.to_string(), default_root), + }; + + if host.is_empty() { + panic!("Invalid remote URI: host is required"); + } + + (user, host, server_root) +} + +#[cfg(test)] +mod tests { + use super::parse_remote_uri; + + #[test] + fn parses_user_host_and_path() { + let (user, host, server_root) = + parse_remote_uri("root@170.205.24.203:/root/playground/hymodtest"); + assert_eq!(user, "root"); + assert_eq!(host, "170.205.24.203"); + assert_eq!(server_root, "/root/playground/hymodtest"); + } + + #[test] + fn parses_user_and_host_without_path() { + let (user, host, server_root) = parse_remote_uri("user@example.com"); + assert_eq!(user, "user"); + assert_eq!(host, "example.com"); + assert_eq!(server_root, "/opt/hytale"); + } + + #[test] + fn parses_host_and_path_with_default_user() { + let (user, host, server_root) = parse_remote_uri("170.205.24.203:/srv/hytale"); + assert_eq!(user, "root"); + assert_eq!(host, "170.205.24.203"); + assert_eq!(server_root, "/srv/hytale"); + } +} diff --git a/features/server/src/cmd/default.rs b/features/server/src/cmd/default.rs index d5bb3b6..10785c9 100644 --- a/features/server/src/cmd/default.rs +++ b/features/server/src/cmd/default.rs @@ -1,19 +1,42 @@ use crate::args::default_args::ServerDefaultArgs; -use core_config::server::load_server_config; +use core_config::server::{load_server_config, ServerKind}; pub fn run(args: ServerDefaultArgs) { - if load_server_config(&args.name).is_err() { + let expected_kind = match args.kind.as_str() { + "local" => ServerKind::Local, + "remote" | "ssh" => ServerKind::Remote, + _ => { + eprintln!( + "Invalid server kind: {}. Must be 'local' or 'remote'.", + args.kind + ); + std::process::exit(1); + } + }; + + let config = match load_server_config(&args.name) { + Ok(c) => c, + Err(_) => { + eprintln!( + "Server '{}' does not exist. Please add it first.", + args.name + ); + std::process::exit(1); + } + }; + + if config.server.kind != expected_kind { eprintln!( - "Server '{}' does not exist. Please add it first.", - args.name + "Server '{}' is {:?}, not {:?}.", + args.name, config.server.kind, expected_kind ); std::process::exit(1); } - if let Err(e) = core_config::server::set_default_server(&args.name) { + if let Err(e) = core_config::server::set_default_server_for_kind(&expected_kind, &args.name) { eprintln!("Failed to set default server: {}", e); std::process::exit(1); } - println!("Default server set to '{}'", args.name); + println!("Default {} server set to '{}'", args.kind, args.name); } diff --git a/features/server/src/cmd/list.rs b/features/server/src/cmd/list.rs index cb1fea4..cbe863a 100644 --- a/features/server/src/cmd/list.rs +++ b/features/server/src/cmd/list.rs @@ -1,7 +1,7 @@ use crate::args::list_args::ServerListArgs; use colored::*; use core_config::server::{ - get_default_server, list_servers, load_server_config, ServerConfig, ServerKind, + get_default_server_for_kind, list_servers, load_server_config, ServerConfig, ServerKind, }; pub fn run(args: ServerListArgs) { @@ -13,8 +13,8 @@ pub fn run(args: ServerListArgs) { } }; - let default_server = get_default_server().unwrap_or(None); - let default_name = default_server.as_deref().unwrap_or(""); + let default_local = get_default_server_for_kind(&ServerKind::Local).unwrap_or(None); + let default_remote = get_default_server_for_kind(&ServerKind::Remote).unwrap_or(None); let mut local_servers = Vec::new(); let mut remote_servers = Vec::new(); @@ -42,12 +42,12 @@ pub fn run(args: ServerListArgs) { if !local_servers.is_empty() { println!("\n{}", "Local Servers".bold().underline()); - print_local_table(&local_servers, default_name); + print_local_table(&local_servers, default_local.as_deref().unwrap_or("")); } if !remote_servers.is_empty() { println!("\n{}", "Remote Servers".bold().underline()); - print_remote_table(&remote_servers, default_name); + print_remote_table(&remote_servers, default_remote.as_deref().unwrap_or("")); } if local_servers.is_empty() && remote_servers.is_empty() {