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 8fe5ce1..d05a8ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hymod" -version = "0.0.8" +version = "0.3.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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..38815f8 --- /dev/null +++ b/Makefile @@ -0,0 +1,68 @@ +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 run-built noargs help new build link dev deploy server config install-local + +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 " 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 " + @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 " + @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) + +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) + +install-local: + ./scripts/local_deploy.sh diff --git a/cli/deploy/deploy_command.rs b/cli/deploy/deploy_command.rs index 05e7e28..086ff82 100644 --- a/cli/deploy/deploy_command.rs +++ b/cli/deploy/deploy_command.rs @@ -5,8 +5,14 @@ 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) + #[arg(long, short)] + pub path: Option, } use crate::command::CliCommand; @@ -18,7 +24,9 @@ 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(), }; let plan = features_deploy::generate_plan(args); 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_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..61b3649 100644 --- a/cli/deploy/tests/test_deploy_force_scp.rs +++ b/cli/deploy/tests/test_deploy_force_scp.rs @@ -1,4 +1,39 @@ +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 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() + .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_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 5fc0bfa..9fb30bc 100644 --- a/cli/deploy/tests/test_deploy_transport_rsync_explicit.rs +++ b/cli/deploy/tests/test_deploy_transport_rsync_explicit.rs @@ -1,4 +1,40 @@ +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()) + .env("PATH", "") + .args(["deploy", "prod", "--transport", "rsync", "--dry-run"]) + .assert() + .success() + .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/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/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/src/ascii-art.txt b/cli/src/ascii-art.txt new file mode 100644 index 0000000..41fc870 --- /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 185d120..ec2e761 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,44 @@ impl CliCommand for Commands { } } +fn print_hymod_logo() { + let logo = include_str!("ascii-art.txt"); + print!("{logo}"); + println!(); +} + +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 +115,3 @@ fn main() { std::process::exit(1); } } -// change main diff --git a/cli/tests/deploy_integration.rs b/cli/tests/deploy_integration.rs index dd01fed..edfa445 100644 --- a/cli/tests/deploy_integration.rs +++ b/cli/tests/deploy_integration.rs @@ -1,41 +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/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/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/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 319c9e5..511bafe 100644 --- a/features/deploy/src/lib.rs +++ b/features/deploy/src/lib.rs @@ -1,22 +1,34 @@ 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"); + // 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,32 +37,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 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 = format!("build/libs/{}", jar_name); - // dest_path depends on kind + 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 { @@ -65,35 +62,90 @@ 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 - if args.transport.as_deref() == Some("scp") { - steps.push(Step::UploadScp { + 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, + 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"); } } } - // 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/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..0ec6186 100644 --- a/features/new/tests/test_new_group_package_overrides.rs +++ b/features/new/tests/test_new_group_package_overrides.rs @@ -1,4 +1,74 @@ +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"# v").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: Some("1.2.3".to_string()), + 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!(content.contains("1.2.3"), "Version not replaced in README"); + } + } + } + + 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"); } 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() {