From 2f46e283e58c95f1e2d2d12933b9594380d395d3 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Fri, 26 Jun 2026 13:58:17 -0400 Subject: [PATCH 01/16] feat: add --dry-run flag to DiagnosticFlags and clap args --- src/cli/flags.rs | 3 +++ tests/cli/resolve.rs | 5 ++++- tests/commands/build.rs | 5 ++++- tests/commands/mono/mode.rs | 5 ++++- tests/commands/mono/resolve.rs | 5 ++++- tests/commands/single.rs | 5 ++++- tests/interactive.rs | 5 ++++- 7 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/cli/flags.rs b/src/cli/flags.rs index 782b1e1..48a2b04 100644 --- a/src/cli/flags.rs +++ b/src/cli/flags.rs @@ -115,4 +115,7 @@ pub struct DiagnosticFlags { /// Show timing information for each phase #[arg(long)] pub timing: bool, + /// If set, print commands instead of executing them without making any changes. + #[arg(long)] + pub dry_run: bool, } diff --git a/tests/cli/resolve.rs b/tests/cli/resolve.rs index 35a5760..6c7e765 100644 --- a/tests/cli/resolve.rs +++ b/tests/cli/resolve.rs @@ -75,7 +75,10 @@ fn default_args() -> Args { Args { repo: None, yes: false, - diagnostic: DiagnosticFlags { timing: false }, + diagnostic: DiagnosticFlags { + timing: false, + dry_run: false, + }, connection: ConnectionFlags { ssh: false, https: false, diff --git a/tests/commands/build.rs b/tests/commands/build.rs index cd1fe75..e5bce0a 100644 --- a/tests/commands/build.rs +++ b/tests/commands/build.rs @@ -14,7 +14,10 @@ fn default_resolved(no_build: bool) -> star_setup::cli::ResolvedArgs { let args = Args { repo: Some("user/repo".to_string()), yes: false, - diagnostic: DiagnosticFlags { timing: false }, + diagnostic: DiagnosticFlags { + timing: false, + dry_run: false, + }, connection: ConnectionFlags { ssh: false, https: false, diff --git a/tests/commands/mono/mode.rs b/tests/commands/mono/mode.rs index 12e49ef..185723d 100644 --- a/tests/commands/mono/mode.rs +++ b/tests/commands/mono/mode.rs @@ -14,7 +14,10 @@ fn default_resolved_mono(repos: Vec) -> star_setup::cli::ResolvedArgs { let args = Args { repo: Some("user/test-repo".to_string()), yes: true, - diagnostic: DiagnosticFlags { timing: false }, + diagnostic: DiagnosticFlags { + timing: false, + dry_run: false, + }, connection: ConnectionFlags { ssh: false, https: false, diff --git a/tests/commands/mono/resolve.rs b/tests/commands/mono/resolve.rs index 9dcf779..c95edf9 100644 --- a/tests/commands/mono/resolve.rs +++ b/tests/commands/mono/resolve.rs @@ -55,7 +55,10 @@ fn default_resolved() -> star_setup::cli::ResolvedArgs { let args = Args { repo: Some("user/repo".to_string()), yes: false, - diagnostic: DiagnosticFlags { timing: false }, + diagnostic: DiagnosticFlags { + timing: false, + dry_run: false, + }, connection: ConnectionFlags { ssh: false, https: false, diff --git a/tests/commands/single.rs b/tests/commands/single.rs index 1b96705..ad9d5a3 100644 --- a/tests/commands/single.rs +++ b/tests/commands/single.rs @@ -14,7 +14,10 @@ fn default_resolved() -> star_setup::cli::ResolvedArgs { let args = Args { repo: Some("user/repo".to_string()), yes: false, - diagnostic: DiagnosticFlags { timing: false }, + diagnostic: DiagnosticFlags { + timing: false, + dry_run: false, + }, connection: ConnectionFlags { ssh: false, https: false, diff --git a/tests/interactive.rs b/tests/interactive.rs index d10a132..9472145 100644 --- a/tests/interactive.rs +++ b/tests/interactive.rs @@ -13,7 +13,10 @@ fn default_resolved() -> star_setup::cli::ResolvedArgs { let args = Args { repo: None, yes: false, - diagnostic: DiagnosticFlags { timing: false }, + diagnostic: DiagnosticFlags { + timing: false, + dry_run: false, + }, connection: ConnectionFlags { ssh: false, https: false, From 256cdad4050de9bc1cbf19b1bb9e905a679f837b Mon Sep 17 00:00:00 2001 From: Masonlet Date: Fri, 26 Jun 2026 14:01:33 -0400 Subject: [PATCH 02/16] feat: add DryRunRunner --- src/ctx.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/ctx.rs b/src/ctx.rs index 8e20edf..2b734f2 100644 --- a/src/ctx.rs +++ b/src/ctx.rs @@ -28,9 +28,22 @@ pub struct RunCtx<'a> { pub runner: &'a mut dyn Runner, } +/// Runner that executes commands. pub struct ProcessRunner; impl Runner for ProcessRunner { fn run(&mut self, cmd: &[&str], cwd: Option<&Path>, io: &mut IoCtx<'_>) -> Result<(), String> { run_command(cmd, cwd, io.verbose, io.output) } } + +/// Runner that prints commands instead of executing them. +pub struct DryRunRunner; +impl Runner for DryRunRunner { + fn run(&mut self, cmd: &[&str], cwd: Option<&Path>, io: &mut IoCtx<'_>) -> Result<(), String> { + writeln!(io.output, "Would run: {}", cmd.join(" ")).map_err(|e| e.to_string())?; + if let Some(dir) = cwd { + writeln!(io.output, " in directory: {}", dir.display()).map_err(|e| e.to_string())?; + } + Ok(()) + } +} From dba63ea3a914d051a936a3e8c8bff4353c2b2548 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Fri, 26 Jun 2026 14:08:15 -0400 Subject: [PATCH 03/16] feat: integrate dry_run into IoCtx and resolved types --- src/cli/resolve.rs | 8 +++++++- src/cli/resolved.rs | 1 + src/config/crud.rs | 1 + src/config/types.rs | 3 +++ src/ctx.rs | 1 + src/run.rs | 1 + tests/cli/build/detect.rs | 12 ++++++++++++ tests/cli/resolve.rs | 5 +++++ tests/commands/display.rs | 1 + tests/commands/single.rs | 6 +++++- tests/common/io.rs | 1 + tests/config/crud.rs | 1 + tests/config/fixtures.rs | 1 + tests/config/io.rs | 1 + tests/ctx.rs | 1 + tests/repository.rs | 1 + tests/utils/prerequisites.rs | 1 + 17 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/cli/resolve.rs b/src/cli/resolve.rs index 37abf65..adcee24 100644 --- a/src/cli/resolve.rs +++ b/src/cli/resolve.rs @@ -50,6 +50,12 @@ pub fn resolve_with_config(mut args: Args, config: &SetupConfig) -> Result Result()? diff --git a/src/cli/resolved.rs b/src/cli/resolved.rs index f46f162..92aba64 100644 --- a/src/cli/resolved.rs +++ b/src/cli/resolved.rs @@ -9,6 +9,7 @@ pub struct ResolvedConnectionFlags { /// Resolved diagnostic flags after applying config and CLI overrides. pub struct ResolvedDiagnosticFlags { pub timing: bool, + pub dry_run: bool, } /// Resolved build flags after applying config and CLI overrides. diff --git a/src/config/crud.rs b/src/config/crud.rs index a53cfa2..d239ec9 100644 --- a/src/config/crud.rs +++ b/src/config/crud.rs @@ -50,6 +50,7 @@ pub fn create_default_config(path: PathBuf, yes: bool, io: &mut IoCtx<'_>) -> Re clean: false, verbose: false, timing: false, + dry_run: false, cmake_flags: vec![], meson_flags: vec![], }, diff --git a/src/config/types.rs b/src/config/types.rs index 75c524b..ed1b55e 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -22,6 +22,8 @@ pub struct ConfigEntry { pub verbose: bool, /// Show timing information. pub timing: bool, + /// Print commands instead of executing them. + pub dry_run: bool, /// Additional `CMake` arguments. pub cmake_flags: Vec, /// Additional `Meson` arguments. @@ -39,6 +41,7 @@ impl From<&ResolvedArgs> for ConfigEntry { clean: args.build.clean, verbose: args.connection.verbose, timing: args.diagnostic.timing, + dry_run: args.diagnostic.dry_run, cmake_flags: args.build.cmake_flags.clone(), meson_flags: args.build.meson_flags.clone(), } diff --git a/src/ctx.rs b/src/ctx.rs index 2b734f2..8c910fb 100644 --- a/src/ctx.rs +++ b/src/ctx.rs @@ -10,6 +10,7 @@ pub struct IoCtx<'a> { pub output: &'a mut dyn Write, pub verbose: bool, pub timing: bool, + pub dry_run: bool, } /// Trait for executing shell commands. diff --git a/src/run.rs b/src/run.rs index 4715ff8..76dcc66 100644 --- a/src/run.rs +++ b/src/run.rs @@ -85,6 +85,7 @@ pub fn run() -> Result<(), Box> { output: &mut stdout, verbose: args.connection.verbose, timing: args.diagnostic.timing, + dry_run: args.diagnostic.dry_run, }; if handle_early_commands(&args, &mut config, &mut io)? { diff --git a/tests/cli/build/detect.rs b/tests/cli/build/detect.rs index 45a6d2b..54d0001 100644 --- a/tests/cli/build/detect.rs +++ b/tests/cli/build/detect.rs @@ -26,6 +26,7 @@ fn test_detect_build_system_none() { output: &mut Vec::new(), verbose: false, timing: false, + dry_run: false, }, runner: &mut runner, }; @@ -43,6 +44,7 @@ fn test_detect_build_system_cmake() { output: &mut Vec::new(), verbose: false, timing: false, + dry_run: false, }, runner: &mut runner, }; @@ -60,6 +62,7 @@ fn test_detect_build_system_meson() { output: &mut Vec::new(), verbose: false, timing: false, + dry_run: false, }, runner: &mut runner, }; @@ -78,6 +81,7 @@ fn test_detect_build_system_both_picks_cmake() { output: &mut Vec::new(), verbose: false, timing: false, + dry_run: false, }, runner: &mut runner, }; @@ -96,6 +100,7 @@ fn test_detect_build_system_both_picks_meson() { output: &mut Vec::new(), verbose: false, timing: false, + dry_run: false, }, runner: &mut runner, }; @@ -114,6 +119,7 @@ fn test_detect_build_system_timing_output() { output: &mut output, verbose: false, timing: true, + dry_run: false, }, runner: &mut runner, }; @@ -132,6 +138,7 @@ fn test_detect_mono_build_system_cmake() { output: &mut Vec::new(), verbose: false, timing: false, + dry_run: false, }, runner: &mut runner, }; @@ -149,6 +156,7 @@ fn test_detect_mono_build_system_meson() { output: &mut Vec::new(), verbose: false, timing: false, + dry_run: false, }, runner: &mut runner, }; @@ -166,6 +174,7 @@ fn test_detect_mono_build_system_none() { output: &mut Vec::new(), verbose: false, timing: false, + dry_run: false, }, runner: &mut runner, }; @@ -184,6 +193,7 @@ fn test_detect_mono_build_system_both_picks_cmake() { output: &mut Vec::new(), verbose: false, timing: false, + dry_run: false, }, runner: &mut runner, }; @@ -202,6 +212,7 @@ fn test_detect_mono_build_system_both_picks_meson() { output: &mut Vec::new(), verbose: false, timing: false, + dry_run: false, }, runner: &mut runner, }; @@ -220,6 +231,7 @@ fn test_detect_mono_build_system_timing_output() { output: &mut output, verbose: false, timing: true, + dry_run: false, }, runner: &mut runner, }; diff --git a/tests/cli/resolve.rs b/tests/cli/resolve.rs index 6c7e765..d88c4b5 100644 --- a/tests/cli/resolve.rs +++ b/tests/cli/resolve.rs @@ -144,6 +144,7 @@ fn test_resolve_with_config_applies_config_defaults() { no_build: true, clean: true, timing: false, + dry_run: false, cmake_flags: vec!["-DTEST=ON".to_string()], meson_flags: vec![], }, @@ -172,6 +173,7 @@ fn test_resolve_with_config_cli_overrides_config() { no_build: false, clean: false, timing: false, + dry_run: false, cmake_flags: vec![], meson_flags: vec![], }, @@ -225,6 +227,7 @@ fn test_resolve_with_config_named_config_pulls_correct_values() { no_build: false, clean: true, timing: false, + dry_run: false, cmake_flags: vec![], meson_flags: vec![], }, @@ -252,6 +255,7 @@ fn test_resolve_with_config_cli_cmake_flags_not_overwritten_by_config() { no_build: false, clean: false, timing: false, + dry_run: false, cmake_flags: vec!["-DCONFIG_FLAG=ON".to_string()], meson_flags: vec![], }, @@ -276,6 +280,7 @@ fn test_resolve_with_config_negative_flags_override_config() { no_build: true, clean: true, timing: false, + dry_run: false, cmake_flags: vec![], meson_flags: vec![], }, diff --git a/tests/commands/display.rs b/tests/commands/display.rs index 65eb7f0..8323e7f 100644 --- a/tests/commands/display.rs +++ b/tests/commands/display.rs @@ -58,6 +58,7 @@ fn test_print_setup_complete_timing() { output: &mut output, verbose: false, timing: true, + dry_run: false, }; print_setup_complete( diff --git a/tests/commands/single.rs b/tests/commands/single.rs index ad9d5a3..dcb755a 100644 --- a/tests/commands/single.rs +++ b/tests/commands/single.rs @@ -119,7 +119,10 @@ fn test_single_repo_mode_cleans_build_dir() { let mut input = b"n\n".as_ref(); let mut output = sink(); let mut runner = MockRunner::new(); - let mut ctx = RunCtx { io: make_io(&mut input, &mut output), runner: &mut runner }; + let mut ctx = RunCtx { + io: make_io(&mut input, &mut output), + runner: &mut runner, + }; single_repo_mode(&args, tmp.path(), &mut ctx).unwrap(); assert!(!build_dir.join("dummy.txt").exists()); @@ -141,6 +144,7 @@ fn test_single_repo_mode_outputs_timing() { output: &mut output, verbose: false, timing: true, + dry_run: false, }, runner: &mut runner, }; diff --git a/tests/common/io.rs b/tests/common/io.rs index a7bc247..05aacf7 100644 --- a/tests/common/io.rs +++ b/tests/common/io.rs @@ -17,5 +17,6 @@ pub fn make_io<'a>( output, verbose: false, timing: false, + dry_run: false, } } diff --git a/tests/config/crud.rs b/tests/config/crud.rs index b551784..17c2e48 100644 --- a/tests/config/crud.rs +++ b/tests/config/crud.rs @@ -62,6 +62,7 @@ fn test_add_config_aborts_when_exists_and_not_confirmed() { clean: false, verbose: false, timing: false, + dry_run: false, cmake_flags: vec![], meson_flags: vec![], }, diff --git a/tests/config/fixtures.rs b/tests/config/fixtures.rs index 832a920..7fcc811 100644 --- a/tests/config/fixtures.rs +++ b/tests/config/fixtures.rs @@ -10,6 +10,7 @@ pub fn sample_entry() -> ConfigEntry { clean: true, verbose: false, timing: false, + dry_run: false, cmake_flags: vec![], meson_flags: vec![], } diff --git a/tests/config/io.rs b/tests/config/io.rs index 0a48f37..0b83232 100644 --- a/tests/config/io.rs +++ b/tests/config/io.rs @@ -23,6 +23,7 @@ fn test_save_and_load_roundtrip() { clean: false, verbose: false, timing: false, + dry_run: false, cmake_flags: vec![], meson_flags: vec![], }, diff --git a/tests/ctx.rs b/tests/ctx.rs index 3737474..04f444a 100644 --- a/tests/ctx.rs +++ b/tests/ctx.rs @@ -9,6 +9,7 @@ fn test_process_runner_runs_command() { output: &mut output, verbose: false, timing: false, + dry_run: false, }; assert!(runner.run(&["git", "--version"], None, &mut io).is_ok()); } diff --git a/tests/repository.rs b/tests/repository.rs index 2722fbf..0987076 100644 --- a/tests/repository.rs +++ b/tests/repository.rs @@ -77,6 +77,7 @@ fn test_clone_skips_existing_directory() { output: &mut Vec::new(), verbose: false, timing: false, + dry_run: false, }, runner: &mut runner, }; diff --git a/tests/utils/prerequisites.rs b/tests/utils/prerequisites.rs index 6d3331c..1b9f3b4 100644 --- a/tests/utils/prerequisites.rs +++ b/tests/utils/prerequisites.rs @@ -18,6 +18,7 @@ fn test_check_prerequisites_verbose_outputs_found() { output: &mut output, verbose: true, timing: false, + dry_run: false, }; check_prerequisites(&mut io).unwrap(); let out = String::from_utf8(output).unwrap(); From 81165b69d0da05c8ff0fbd99bee549107e155fd4 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Fri, 26 Jun 2026 14:09:26 -0400 Subject: [PATCH 04/16] feat: wire DryRunRunner into run() based on dry_run flag --- src/run.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/run.rs b/src/run.rs index 76dcc66..948a96d 100644 --- a/src/run.rs +++ b/src/run.rs @@ -5,7 +5,7 @@ use crate::{ add_config, create_default_config, list_configs, load_config, remove_config, ConfigEntry, SetupConfig, }, - ctx::{IoCtx, ProcessRunner, RunCtx}, + ctx::{IoCtx, ProcessRunner, DryRunRunner, Runner, RunCtx}, interactive::interactive_mode, profile::{add_profile, list_profiles, remove_profile}, utils::check_prerequisites, @@ -102,11 +102,10 @@ pub fn run() -> Result<(), Box> { check_prerequisites(&mut io)?; - let mut runner = ProcessRunner; - let mut ctx = RunCtx { - io, - runner: &mut runner, - }; + let mut dry = DryRunRunner; + let mut real = ProcessRunner; + let runner: &mut dyn Runner = if io.dry_run { &mut dry } else { &mut real }; + let mut ctx = RunCtx { io, runner }; if args.mono.mono_repo { mono_repo_mode(&args, &config, Path::new("."), &mut ctx)?; From c29a264b8cfe6e6553fdb5fe1130ce9b45f2e600 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Fri, 26 Jun 2026 14:12:18 -0400 Subject: [PATCH 05/16] test: add DryRunRunner tests --- tests/ctx.rs | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/tests/ctx.rs b/tests/ctx.rs index 04f444a..a1abe89 100644 --- a/tests/ctx.rs +++ b/tests/ctx.rs @@ -1,6 +1,8 @@ +use star_setup::ctx::{DryRunRunner, IoCtx, ProcessRunner, Runner}; +use std::path::Path; + #[test] fn test_process_runner_runs_command() { - use star_setup::ctx::{IoCtx, ProcessRunner, Runner}; let mut input = b"".as_ref(); let mut output = Vec::new(); let mut runner = ProcessRunner; @@ -13,3 +15,37 @@ fn test_process_runner_runs_command() { }; assert!(runner.run(&["git", "--version"], None, &mut io).is_ok()); } + +#[test] +fn test_dry_run_runner_prints_command() { + let mut input = b"".as_ref(); + let mut output = Vec::new(); + let mut runner = DryRunRunner; + let mut io = IoCtx { + input: &mut input, + output: &mut output, + verbose: false, + timing: false, + dry_run: true, + }; + runner.run(&["git", "clone", "foo"], None, &mut io).unwrap(); + assert_eq!(String::from_utf8(output).unwrap(), "Would run: git clone foo\n"); +} + +#[test] +fn test_dry_run_runner_prints_cwd() { + let mut input = b"".as_ref(); + let mut output = Vec::new(); + let mut runner = DryRunRunner; + let mut io = IoCtx { + input: &mut input, + output: &mut output, + verbose: false, + timing: false, + dry_run: true, + }; + runner.run(&["cmake", ".."], Some(Path::new("/tmp/build")), &mut io).unwrap(); + let out = String::from_utf8(output).unwrap(); + assert!(out.contains("Would run: cmake ..")); + assert!(out.contains(" in directory: /tmp/build")); +} From 2f793b9476488fcb09b6535a3d88a3bf48ced515 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Fri, 26 Jun 2026 14:48:51 -0400 Subject: [PATCH 06/16] feat: implement dry-run for filesystem ops and mode function --- src/commands/mono/display.rs | 87 +++++++++++++++++++++++------------- src/commands/mono/mode.rs | 74 ++++++++++++++++++++---------- src/commands/setup.rs | 30 ++++++++++--- src/commands/single.rs | 7 ++- src/run.rs | 2 +- tests/commands/display.rs | 33 ++++---------- tests/ctx.rs | 9 +++- 7 files changed, 153 insertions(+), 89 deletions(-) diff --git a/src/commands/mono/display.rs b/src/commands/mono/display.rs index 6dfa202..9f2b792 100644 --- a/src/commands/mono/display.rs +++ b/src/commands/mono/display.rs @@ -1,43 +1,70 @@ use crate::{ctx::IoCtx, repository::repo_dir_name}; -use std::{borrow::Cow, collections::HashMap, path::Path}; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; -/// Prints the setup completion summary including paths, executable location, and total timing. -pub fn print_setup_complete( +pub struct SetupPaths { + pub mono_repo_disp: PathBuf, + pub exe_path: Option, + pub build_disp: Option, +} + +/// Resolves display paths for setup completion summary. +#[must_use] +pub fn resolve_setup_paths( canonical_map: Option<&HashMap>, mono_repo_path: &Path, build_path: &Path, test_repo: &str, - total: std::time::Instant, - io: &mut IoCtx<'_>, -) { - writeln!(io.output, "Setup complete").ok(); +) -> SetupPaths { let mono_repo_disp = - dunce::canonicalize(mono_repo_path).map_or_else(|_| Cow::Borrowed(mono_repo_path), Cow::Owned); - writeln!(io.output, "Repositories in: {}", mono_repo_disp.display()).ok(); + dunce::canonicalize(mono_repo_path).unwrap_or_else(|_| mono_repo_path.to_path_buf()); - if let Some(map) = canonical_map { + let (exe_path, build_disp) = if let Some(map) = canonical_map { let test_repo_name = repo_dir_name(test_repo); - if let Some((canonical, _)) = map.iter().find(|(_, v)| *v == &test_repo_name) { - let exe_name = if cfg!(windows) { - format!("{canonical}.exe") - } else { - canonical.clone() - }; - let exe_path = build_path - .join("repos") - .join(&test_repo_name) - .join(&exe_name); - writeln!( - io.output, - "Executable: {}", - dunce::canonicalize(&exe_path).unwrap_or(exe_path).display() - ) - .ok(); - } + let exe_path = map + .iter() + .find(|(_, v)| *v == &test_repo_name) + .map(|(canonical, _)| { + let exe_name = if cfg!(windows) { + format!("{canonical}.exe") + } else { + canonical.clone() + }; + let p = build_path + .join("repos") + .join(&test_repo_name) + .join(&exe_name); + dunce::canonicalize(&p).unwrap_or(p) + }); + (exe_path, None) } else { - let build_disp = - dunce::canonicalize(build_path).map_or_else(|_| Cow::Borrowed(build_path), Cow::Owned); - writeln!(io.output, "Build output in: {}", build_disp.display()).ok(); + let build_disp = dunce::canonicalize(build_path).unwrap_or_else(|_| build_path.to_path_buf()); + (None, Some(build_disp)) + }; + + SetupPaths { + mono_repo_disp, + exe_path, + build_disp, + } +} + +/// Prints the setup completion summary. +pub fn print_setup_complete(paths: &SetupPaths, total: std::time::Instant, io: &mut IoCtx<'_>) { + writeln!(io.output, "Setup complete").ok(); + writeln!( + io.output, + "Repositories in: {}", + paths.mono_repo_disp.display() + ) + .ok(); + if let Some(exe) = &paths.exe_path { + writeln!(io.output, "Executable: {}", exe.display()).ok(); + } + if let Some(build) = &paths.build_disp { + writeln!(io.output, "Build output in: {}", build.display()).ok(); } if io.timing { writeln!(io.output, "[timing] Total: {:.2?}", total.elapsed()).ok(); diff --git a/src/commands/mono/mode.rs b/src/commands/mono/mode.rs index aff4d83..90c480c 100644 --- a/src/commands/mono/mode.rs +++ b/src/commands/mono/mode.rs @@ -2,7 +2,11 @@ use crate::{ cli::{detect_mono_build_system, ResolvedArgs}, commands::{ build_repo_list, configure_and_build, extract_repo_input, - mono::{clone_mono_repos, generate_mono_config, print_setup_complete}, + mono::{ + clone_mono_repos, + display::{resolve_setup_paths, SetupPaths}, + generate_mono_config, print_setup_complete, + }, prepare_build_dir, resolve_repos_for_mono, resolve_test_repo, }, config::SetupConfig, @@ -33,9 +37,18 @@ pub fn mono_repo_mode( let mono_repo_path = base_dir.join(&args.mono.mono_dir); let repos_path = mono_repo_path.join("repos"); - crate::time!(ctx.io.timing, ctx.io.output, "Create directory", { - fs::create_dir_all(&repos_path).map_err(|e| e.to_string())?; - }); + if ctx.io.dry_run { + writeln!( + ctx.io.output, + "Would run: fs::create_dir_all({})", + repos_path.display() + ) + .ok(); + } else { + crate::time!(ctx.io.timing, ctx.io.output, "Create directory", { + fs::create_dir_all(&repos_path).map_err(|e| e.to_string())?; + }); + } clone_mono_repos(&repos, &repos_path, args.connection.ssh, ctx)?; @@ -44,28 +57,41 @@ pub fn mono_repo_mode( .map(|r| repos_path.join(repo_dir_name(r))) .collect(); - let build_system = detect_mono_build_system(&repo_dirs, ctx)?; + let build_path = mono_repo_path.join(&args.build.build_dir); - let canonical_map = generate_mono_config( - build_system, - &mono_repo_path, - &repos_path, - &repo_dirs, - &repos, - ctx, - )?; + let canonical_map = if ctx.io.dry_run { + prepare_build_dir(build_path.as_path(), args.build.clean, ctx)?; + None + } else { + let build_system = detect_mono_build_system(&repo_dirs, ctx)?; + let map = generate_mono_config( + build_system, + &mono_repo_path, + &repos_path, + &repo_dirs, + &repos, + ctx, + )?; + prepare_build_dir(build_path.as_path(), args.build.clean, ctx)?; + configure_and_build(args, &mono_repo_path, &build_path, build_system, true, ctx)?; + map + }; - let build_path = mono_repo_path.join(&args.build.build_dir); - prepare_build_dir(build_path.as_path(), args.build.clean, ctx)?; - configure_and_build(args, &mono_repo_path, &build_path, build_system, true, ctx)?; + let paths = if ctx.io.dry_run { + SetupPaths { + mono_repo_disp: mono_repo_path.clone(), + exe_path: None, + build_disp: Some(build_path.clone()), + } + } else { + resolve_setup_paths( + canonical_map.as_ref(), + &mono_repo_path, + &build_path, + &test_repo, + ) + }; - print_setup_complete( - canonical_map.as_ref(), - &mono_repo_path, - &build_path, - &test_repo, - total, - &mut ctx.io, - ); + print_setup_complete(&paths, total, &mut ctx.io); Ok(()) } diff --git a/src/commands/setup.rs b/src/commands/setup.rs index 2b6249c..d0a5953 100644 --- a/src/commands/setup.rs +++ b/src/commands/setup.rs @@ -15,15 +15,33 @@ pub fn prepare_build_dir( ) -> Result<(), String> { if clean && build_path.exists() { writeln!(ctx.io.output, "Cleaning build directory\n").ok(); - crate::time!(ctx.io.timing, ctx.io.output, "Clean", { - fs::remove_dir_all(build_path).map_err(|e| e.to_string())?; - }); + if ctx.io.dry_run { + writeln!( + ctx.io.output, + "Would run: fs::remove_dir_all({})", + build_path.display() + ) + .ok(); + } else { + crate::time!(ctx.io.timing, ctx.io.output, "Clean", { + fs::remove_dir_all(build_path).map_err(|e| e.to_string())?; + }); + } } writeln!(ctx.io.output, "Creating build directory\n").ok(); - crate::time!(ctx.io.timing, ctx.io.output, "Create build directory", { - fs::create_dir_all(build_path).map_err(|e| e.to_string())?; - }); + if ctx.io.dry_run { + writeln!( + ctx.io.output, + "Would run: fs::create_dir_all({})", + build_path.display() + ) + .ok(); + } else { + crate::time!(ctx.io.timing, ctx.io.output, "Create build directory", { + fs::create_dir_all(build_path).map_err(|e| e.to_string())?; + }); + } Ok(()) } diff --git a/src/commands/single.rs b/src/commands/single.rs index 9879ac4..1cfb605 100644 --- a/src/commands/single.rs +++ b/src/commands/single.rs @@ -58,8 +58,11 @@ pub fn single_repo_mode( let build_path = repo_path.join(&args.build.build_dir); prepare_build_dir(&build_path, args.build.clean, ctx)?; - let build_system = detect_build_system(&repo_path, ctx)?; - configure_and_build(args, &repo_path, &build_path, build_system, false, ctx)?; + + if !ctx.io.dry_run { + let build_system = detect_build_system(&repo_path, ctx)?; + configure_and_build(args, &repo_path, &build_path, build_system, false, ctx)?; + } writeln!( ctx.io.output, diff --git a/src/run.rs b/src/run.rs index 948a96d..202fdda 100644 --- a/src/run.rs +++ b/src/run.rs @@ -5,7 +5,7 @@ use crate::{ add_config, create_default_config, list_configs, load_config, remove_config, ConfigEntry, SetupConfig, }, - ctx::{IoCtx, ProcessRunner, DryRunRunner, Runner, RunCtx}, + ctx::{DryRunRunner, IoCtx, ProcessRunner, RunCtx, Runner}, interactive::interactive_mode, profile::{add_profile, list_profiles, remove_profile}, utils::check_prerequisites, diff --git a/tests/commands/display.rs b/tests/commands/display.rs index 8323e7f..a0ee00c 100644 --- a/tests/commands/display.rs +++ b/tests/commands/display.rs @@ -1,5 +1,5 @@ use super::common::{empty_input, make_io, sink}; -use star_setup::commands::mono::display::print_setup_complete; +use star_setup::commands::mono::display::{print_setup_complete, resolve_setup_paths}; use std::collections::HashMap; use tempfile::TempDir; @@ -9,16 +9,13 @@ fn test_print_setup_complete_no_map() { let mut input = empty_input(); let mut output = sink(); let mut io = make_io(&mut input, &mut output); - - print_setup_complete( - None::>.as_ref(), + let paths = resolve_setup_paths( + None::<&HashMap>, tmp.path(), &tmp.path().join("build"), "user/repo", - std::time::Instant::now(), - &mut io, ); - + print_setup_complete(&paths, std::time::Instant::now(), &mut io); let out = String::from_utf8(output).unwrap(); assert!(out.contains("Setup complete")); assert!(out.contains("Build output in:")); @@ -30,19 +27,10 @@ fn test_print_setup_complete_with_map() { let mut input = empty_input(); let mut output = sink(); let mut io = make_io(&mut input, &mut output); - let mut map = HashMap::new(); map.insert("my_lib".to_string(), "user-repo".to_string()); - - print_setup_complete( - Some(&map), - tmp.path(), - &tmp.path().join("build"), - "user/repo", - std::time::Instant::now(), - &mut io, - ); - + let paths = resolve_setup_paths(Some(&map), tmp.path(), &tmp.path().join("build"), "user/repo"); + print_setup_complete(&paths, std::time::Instant::now(), &mut io); let out = String::from_utf8(output).unwrap(); assert!(out.contains("Setup complete")); assert!(out.contains("Executable:")); @@ -60,16 +48,13 @@ fn test_print_setup_complete_timing() { timing: true, dry_run: false, }; - - print_setup_complete( - None::>.as_ref(), + let paths = resolve_setup_paths( + None::<&HashMap>, tmp.path(), &tmp.path().join("build"), "user/repo", - std::time::Instant::now(), - &mut io, ); - + print_setup_complete(&paths, std::time::Instant::now(), &mut io); let out = String::from_utf8(output).unwrap(); assert!(out.contains("[timing] Total:")); } diff --git a/tests/ctx.rs b/tests/ctx.rs index a1abe89..e12095c 100644 --- a/tests/ctx.rs +++ b/tests/ctx.rs @@ -29,7 +29,10 @@ fn test_dry_run_runner_prints_command() { dry_run: true, }; runner.run(&["git", "clone", "foo"], None, &mut io).unwrap(); - assert_eq!(String::from_utf8(output).unwrap(), "Would run: git clone foo\n"); + assert_eq!( + String::from_utf8(output).unwrap(), + "Would run: git clone foo\n" + ); } #[test] @@ -44,7 +47,9 @@ fn test_dry_run_runner_prints_cwd() { timing: false, dry_run: true, }; - runner.run(&["cmake", ".."], Some(Path::new("/tmp/build")), &mut io).unwrap(); + runner + .run(&["cmake", ".."], Some(Path::new("/tmp/build")), &mut io) + .unwrap(); let out = String::from_utf8(output).unwrap(); assert!(out.contains("Would run: cmake ..")); assert!(out.contains(" in directory: /tmp/build")); From d4406919794555242b35b0f5e72bd3ce85f61c5e Mon Sep 17 00:00:00 2001 From: Masonlet Date: Fri, 26 Jun 2026 14:53:32 -0400 Subject: [PATCH 07/16] fix: print clean during dry_run regardless of build dir existence --- src/commands/setup.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/commands/setup.rs b/src/commands/setup.rs index d0a5953..c45291e 100644 --- a/src/commands/setup.rs +++ b/src/commands/setup.rs @@ -13,20 +13,20 @@ pub fn prepare_build_dir( clean: bool, ctx: &mut RunCtx<'_>, ) -> Result<(), String> { - if clean && build_path.exists() { + if clean && ctx.io.dry_run { writeln!(ctx.io.output, "Cleaning build directory\n").ok(); - if ctx.io.dry_run { - writeln!( - ctx.io.output, - "Would run: fs::remove_dir_all({})", - build_path.display() - ) - .ok(); - } else { - crate::time!(ctx.io.timing, ctx.io.output, "Clean", { - fs::remove_dir_all(build_path).map_err(|e| e.to_string())?; - }); - } + writeln!( + ctx.io.output, + "Would run: fs::remove_dir_all({})", + build_path.display() + ) + .ok(); + } + else if clean && build_path.exists() { + writeln!(ctx.io.output, "Cleaning build directory\n").ok(); + crate::time!(ctx.io.timing, ctx.io.output, "Clean", { + fs::remove_dir_all(build_path).map_err(|e| e.to_string())?; + }); } writeln!(ctx.io.output, "Creating build directory\n").ok(); From 20f30dca0ee3d4e6f58995a3b7a5755d0aa9d755 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Fri, 26 Jun 2026 14:59:36 -0400 Subject: [PATCH 08/16] chore: bump v0.3.1 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index be0b86a..ab5f76a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "star-setup" -version = "0.3.0" +version = "0.3.1" edition = "2021" repository = "https://github.com/star-setup/core" description = "Lightweight CLI to clone, configure, and wire single or multi-repo ecosystems" From 64ab18eac6f123cc0052188142ec5a65903f4bbf Mon Sep 17 00:00:00 2001 From: Masonlet Date: Fri, 26 Jun 2026 15:02:14 -0400 Subject: [PATCH 09/16] chore: cargo fmt --- src/commands/setup.rs | 3 +-- tests/commands/display.rs | 7 ++++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/commands/setup.rs b/src/commands/setup.rs index c45291e..3b4a91f 100644 --- a/src/commands/setup.rs +++ b/src/commands/setup.rs @@ -21,8 +21,7 @@ pub fn prepare_build_dir( build_path.display() ) .ok(); - } - else if clean && build_path.exists() { + } else if clean && build_path.exists() { writeln!(ctx.io.output, "Cleaning build directory\n").ok(); crate::time!(ctx.io.timing, ctx.io.output, "Clean", { fs::remove_dir_all(build_path).map_err(|e| e.to_string())?; diff --git a/tests/commands/display.rs b/tests/commands/display.rs index a0ee00c..8c02962 100644 --- a/tests/commands/display.rs +++ b/tests/commands/display.rs @@ -29,7 +29,12 @@ fn test_print_setup_complete_with_map() { let mut io = make_io(&mut input, &mut output); let mut map = HashMap::new(); map.insert("my_lib".to_string(), "user-repo".to_string()); - let paths = resolve_setup_paths(Some(&map), tmp.path(), &tmp.path().join("build"), "user/repo"); + let paths = resolve_setup_paths( + Some(&map), + tmp.path(), + &tmp.path().join("build"), + "user/repo", + ); print_setup_complete(&paths, std::time::Instant::now(), &mut io); let out = String::from_utf8(output).unwrap(); assert!(out.contains("Setup complete")); From ff4e77c8a93631b49e8f53693cc83c5fcbfb524a Mon Sep 17 00:00:00 2001 From: Masonlet Date: Fri, 26 Jun 2026 15:14:27 -0400 Subject: [PATCH 10/16] docs: add doc comments to SetupPaths --- src/commands/mono/display.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/commands/mono/display.rs b/src/commands/mono/display.rs index 9f2b792..a7a4480 100644 --- a/src/commands/mono/display.rs +++ b/src/commands/mono/display.rs @@ -4,9 +4,13 @@ use std::{ path::{Path, PathBuf}, }; +/// Resolved display paths for the setup completion summary. pub struct SetupPaths { + /// Canonicalized path to the mono-repo root directory. pub mono_repo_disp: PathBuf, + /// Canonicalized path to the test repository executable, if found. pub exe_path: Option, + /// Canonicalized path to the build output directory, if no canonical map was provided. pub build_disp: Option, } From e86f9478db12ea6952f7ca80e6bb9f0208e9ee6c Mon Sep 17 00:00:00 2001 From: Masonlet Date: Fri, 26 Jun 2026 15:42:38 -0400 Subject: [PATCH 11/16] style: dry-run messages to user-facing text --- src/commands/mono/mode.rs | 2 +- src/commands/setup.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/mono/mode.rs b/src/commands/mono/mode.rs index 90c480c..7d2bef2 100644 --- a/src/commands/mono/mode.rs +++ b/src/commands/mono/mode.rs @@ -40,7 +40,7 @@ pub fn mono_repo_mode( if ctx.io.dry_run { writeln!( ctx.io.output, - "Would run: fs::create_dir_all({})", + "Would create directory: {}", repos_path.display() ) .ok(); diff --git a/src/commands/setup.rs b/src/commands/setup.rs index 3b4a91f..369272b 100644 --- a/src/commands/setup.rs +++ b/src/commands/setup.rs @@ -17,7 +17,7 @@ pub fn prepare_build_dir( writeln!(ctx.io.output, "Cleaning build directory\n").ok(); writeln!( ctx.io.output, - "Would run: fs::remove_dir_all({})", + "Would remove directory: {}", build_path.display() ) .ok(); @@ -32,7 +32,7 @@ pub fn prepare_build_dir( if ctx.io.dry_run { writeln!( ctx.io.output, - "Would run: fs::create_dir_all({})", + "Would create directory: {}", build_path.display() ) .ok(); From 1b973f1055f34fb1cdccc9d6da6794e5dfac0678 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Fri, 26 Jun 2026 15:43:38 -0400 Subject: [PATCH 12/16] test: assert dry-run makes no filesystem changes --- tests/commands/single.rs | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/tests/commands/single.rs b/tests/commands/single.rs index dcb755a..c5d7748 100644 --- a/tests/commands/single.rs +++ b/tests/commands/single.rs @@ -6,7 +6,7 @@ use star_setup::{ }, commands::single_repo_mode, config::SetupConfig, - ctx::RunCtx, + ctx::{DryRunRunner, RunCtx}, }; use tempfile::TempDir; @@ -153,3 +153,28 @@ fn test_single_repo_mode_outputs_timing() { let out = String::from_utf8(output).unwrap(); assert!(out.contains("[timing] Total:")); } + +#[test] +fn test_single_repo_mode_dry_run_makes_no_fs_changes() { + let tmp = TempDir::new().unwrap(); + let mut args = default_resolved(); + args.diagnostic.dry_run = true; + + let mut input = b"".as_ref(); + let mut output = sink(); + let mut runner = DryRunRunner; + let mut ctx = RunCtx { + io: star_setup::ctx::IoCtx { + input: &mut input, + output: &mut output, + verbose: false, + timing: false, + dry_run: true, + }, + runner: &mut runner, + }; + + single_repo_mode(&args, tmp.path(), &mut ctx).unwrap(); + + assert!(!tmp.path().join("user-repo").exists()); +} From 383cb7b6c2ae21169acf816e9648971f6fe0daca Mon Sep 17 00:00:00 2001 From: Masonlet Date: Fri, 26 Jun 2026 18:45:35 -0400 Subject: [PATCH 13/16] test: assert dry-run makes no changes in mono mode --- tests/commands/mono/mode.rs | 27 ++++++++++++++++++++++++++- tests/commands/single.rs | 3 +-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/tests/commands/mono/mode.rs b/tests/commands/mono/mode.rs index 185723d..577dd2c 100644 --- a/tests/commands/mono/mode.rs +++ b/tests/commands/mono/mode.rs @@ -6,7 +6,7 @@ use star_setup::{ }, commands::mono_repo_mode, config::SetupConfig, - ctx::RunCtx, + ctx::{DryRunRunner, RunCtx}, }; use tempfile::TempDir; @@ -86,3 +86,28 @@ fn test_mono_repo_mode_clones_and_configures() { assert!(out.contains("Setup complete")); assert!(out.contains("Total repositories:")); } + +#[test] +fn test_mono_repo_mode_dry_run_makes_no_fs_changes() { + let tmp = TempDir::new().unwrap(); + let mut args = default_resolved_mono(vec!["user/lib1".to_string()]); + args.diagnostic.dry_run = true; + + let mut input = empty_input(); + let mut output = sink(); + let mut runner = DryRunRunner; + let mut ctx = RunCtx { + io: star_setup::ctx::IoCtx { + input: &mut input, + output: &mut output, + verbose: false, + timing: false, + dry_run: true, + }, + runner: &mut runner, + }; + + mono_repo_mode(&args, &SetupConfig::new(), tmp.path(), &mut ctx).unwrap(); + + assert!(std::fs::read_dir(tmp.path()).unwrap().next().is_none()); +} diff --git a/tests/commands/single.rs b/tests/commands/single.rs index c5d7748..defaf68 100644 --- a/tests/commands/single.rs +++ b/tests/commands/single.rs @@ -175,6 +175,5 @@ fn test_single_repo_mode_dry_run_makes_no_fs_changes() { }; single_repo_mode(&args, tmp.path(), &mut ctx).unwrap(); - - assert!(!tmp.path().join("user-repo").exists()); + assert!(std::fs::read_dir(tmp.path()).unwrap().next().is_none()); } From 80ff6fdf892a464f1029974af3963dd7043e3818 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Fri, 26 Jun 2026 18:55:34 -0400 Subject: [PATCH 14/16] feat: respect dry-run in early config and profile commands --- src/config/crud.rs | 77 +++++++++++++++++++++++---------------------- src/profile/crud.rs | 25 +++++++++------ 2 files changed, 56 insertions(+), 46 deletions(-) diff --git a/src/config/crud.rs b/src/config/crud.rs index d239ec9..c74e913 100644 --- a/src/config/crud.rs +++ b/src/config/crud.rs @@ -37,26 +37,28 @@ pub fn create_default_config(path: PathBuf, yes: bool, io: &mut IoCtx<'_>) -> Re return Ok(()); } - let mut config = SetupConfig::new(); - config.path = Some(path.clone()); - config.configs.insert( - "default".to_string(), - ConfigEntry { - ssh: false, - build_type: BuildType::Debug, - build_dir: "build".to_string(), - mono_dir: "build-mono".to_string(), - no_build: false, - clean: false, - verbose: false, - timing: false, - dry_run: false, - cmake_flags: vec![], - meson_flags: vec![], - }, - ); - - save_config(&mut config)?; + if !io.dry_run { + let mut config = SetupConfig::new(); + config.path = Some(path.clone()); + config.configs.insert( + "default".to_string(), + ConfigEntry { + ssh: false, + build_type: BuildType::Debug, + build_dir: "build".to_string(), + mono_dir: "build-mono".to_string(), + no_build: false, + clean: false, + verbose: false, + timing: false, + dry_run: false, + cmake_flags: vec![], + meson_flags: vec![], + }, + ); + + save_config(&mut config)?; + } writeln!( io.output, @@ -93,19 +95,16 @@ pub fn add_config( return Ok(()); } - insert_config(config, name, entry); - let path = save_config(config)?; - - let e = &config.configs[name]; - writeln!( - io.output, - "Configuration '{name}' added successfully to {}", - path.display() - ) - .ok(); - writeln!(io.output, "Configuration details:").ok(); - write!(io.output, "{}", format_entry(e)).ok(); - + if io.dry_run { + writeln!(io.output, "Would save configuration '{name}' to config file").ok(); + } else { + insert_config(config, name, entry); + let path = save_config(config)?; + writeln!(io.output, "Configuration '{name}' added successfully to {}", path.display()).ok(); + let e: &ConfigEntry = &config.configs[name]; + writeln!(io.output, "Configuration details:").ok(); + write!(io.output, "{}", format_entry(e)).ok(); + } Ok(()) } @@ -132,9 +131,13 @@ pub fn remove_config( return Ok(()); } - remove_config_entry(config, name); - let path = save_config(config)?; - writeln!(io.output, "\nConfig '{name}' was successfully removed").ok(); - writeln!(io.output, "Configuration saved to: {}\n", path.display()).ok(); + if io.dry_run { + writeln!(io.output, "Would remove configuration '{name}' from config file").ok(); + } else { + remove_config_entry(config, name); + let path = save_config(config)?; + writeln!(io.output, "\nConfig '{name}' was successfully removed").ok(); + writeln!(io.output, "Configuration saved to: {}\n", path.display()).ok(); + } Ok(()) } diff --git a/src/profile/crud.rs b/src/profile/crud.rs index 8e2d7fb..747ffec 100644 --- a/src/profile/crud.rs +++ b/src/profile/crud.rs @@ -49,11 +49,14 @@ pub fn add_profile( return Ok(()); } - insert_profile(config, &name, repos.clone()); - let path = save_config(config)?; - - writeln!(io.output, "Profile '{name}' added successfully").ok(); - writeln!(io.output, "Configuration saved to: {}", path.display()).ok(); + if io.dry_run { + writeln!(io.output, "Would save profile '{name}' to config file").ok(); + } else { + insert_profile(config, &name, repos.clone()); + let path = save_config(config)?; + writeln!(io.output, "Profile '{name}' added successfully").ok(); + writeln!(io.output, "Configuration saved to: {}", path.display()).ok(); + } print_profile_details(io.output, "Profile details:", "Repositories", &repos); writeln!( io.output, @@ -96,9 +99,13 @@ pub fn remove_profile( return Ok(()); } - remove_profile_entry(config, name); - let path = save_config(config)?; - writeln!(io.output, "\nProfile '{name}' removed successfully").ok(); - writeln!(io.output, "Configuration saved to: {}\n", path.display()).ok(); + if io.dry_run { + writeln!(io.output, "Would remove profile '{name}' from config file").ok(); + } else { + remove_profile_entry(config, name); + let path = save_config(config)?; + writeln!(io.output, "\nProfile '{name}' removed successfully").ok(); + writeln!(io.output, "Configuration saved to: {}\n", path.display()).ok(); + } Ok(()) } From de8da642a3d4ebc59641c8a6170c3f9165f3d9ee Mon Sep 17 00:00:00 2001 From: Masonlet Date: Fri, 26 Jun 2026 18:59:48 -0400 Subject: [PATCH 15/16] test: assert dry-run clean prints would-remove message --- tests/commands/single.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/commands/single.rs b/tests/commands/single.rs index defaf68..fe00d71 100644 --- a/tests/commands/single.rs +++ b/tests/commands/single.rs @@ -177,3 +177,31 @@ fn test_single_repo_mode_dry_run_makes_no_fs_changes() { single_repo_mode(&args, tmp.path(), &mut ctx).unwrap(); assert!(std::fs::read_dir(tmp.path()).unwrap().next().is_none()); } + +#[test] +fn test_single_repo_mode_dry_run_clean_prints_would_remove() { + let tmp = TempDir::new().unwrap(); + let mut args = default_resolved(); + args.diagnostic.dry_run = true; + args.build.clean = true; + + let mut input = b"".as_ref(); + let mut output = Vec::new(); + let mut runner = DryRunRunner; + let mut ctx = RunCtx { + io: star_setup::ctx::IoCtx { + input: &mut input, + output: &mut output, + verbose: false, + timing: false, + dry_run: true, + }, + runner: &mut runner, + }; + + single_repo_mode(&args, tmp.path(), &mut ctx).unwrap(); + + let out = String::from_utf8(output).unwrap(); + assert!(out.contains("Would remove directory:")); + assert!(std::fs::read_dir(tmp.path()).unwrap().next().is_none()); +} From 30b02600d9a002214298b46d4bf9830e4b74bd3a Mon Sep 17 00:00:00 2001 From: Masonlet Date: Fri, 26 Jun 2026 19:04:43 -0400 Subject: [PATCH 16/16] docs: add flags table and clean up usage examples --- README.md | 53 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 10abe6f..8e2bebe 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,37 @@ cargo install --git https://github.com/star-setup/core ## Usage +### Flags +#### Connection +| Flag | Description | +|------|-------------| +| `--ssh` | Clone via SSH instead of HTTPS | +| `--https` | Force HTTPS (default) | +| `--verbose` | Print commands as they run | + +#### Build +| Flag | Description | +|------|-------------| +| `--build-type ` | Build type: `Debug` (default) or `Release` | +| `--build-dir ` | Build output directory (default: `build`) | +| `--no-build` | Configure only, skip build step | +| `--clean` | Remove build directory before configuring | +| `--cmake-arg ` | Pass additional argument to CMake | +| `--meson-arg ` | Pass additional argument to Meson | + +#### Mono-Repo +| Flag | Description | +|------|-------------| +| `--repos ...` | List of dependency repositories | +| `--mono-dir ` | Workspace directory (default: `build-mono`) | +| `--profile ` | Use a saved profile | + +#### Diagnostic +| Flag | Description | +|------|-------------| +| `--dry-run` | Print what would happen without making any changes | +| `--timing` | Show timing for each phase | + ### Interactive Mode Running `star-setup` without arguments launches interactive mode, guiding you through all options. @@ -83,35 +114,19 @@ Interactive mode complete ### Single Repository Mode ```bash -# Clone and build via HTTPS +# Clone and build using a single repository star-setup username/repo - -# Clone and build via SSH -star-setup username/repo --ssh - -# Common flags -star-setup username/repo --build-type Release -star-setup username/repo --build-dir out -star-setup username/repo --no-build -star-setup username/repo --clean -star-setup username/repo --verbose -star-setup username/repo --timing -star-setup username/repo --cmake-arg=-DCMAKE_CXX_COMPILER=clang++ -star-setup username/repo --meson-arg=-Db_lto=true ``` ### Mono-Repo Mode Clones multiple repositories into a single workspace and auto-detects the build system. For CMake projects, generates a root `CMakeLists.txt` wiring all repositories as subdirectories. For Meson projects, generates a root `meson.build` and auto-generates local `.wrap` files bridging canonical dependency names to cloned directories. ```bash -# Manual repo list +# Clone and build a test repo and a manual repo list star-setup username/repo --repos user/lib1 user/lib2 -# Use a saved profile +# Clone and build a test repo and a saved profile star-setup username/repo --profile myprofile - -# With SSH and custom directory -star-setup username/repo --repos user/lib1 user/lib2 --ssh --mono-dir my-workspace ``` #### Workspace Structure (CMake)