From 18e464930c81d3c31b57a4a1fcd0de62328d2291 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 2 Jul 2026 13:11:39 -0400 Subject: [PATCH 01/22] fix: no_dry_run conflicts_with --- src/cli/flags.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/flags.rs b/src/cli/flags.rs index caeeedd..ec0b85f 100644 --- a/src/cli/flags.rs +++ b/src/cli/flags.rs @@ -97,6 +97,6 @@ pub struct DiagnosticFlags { #[arg(long)] pub dry_run: bool, /// Do not use dry-run mode (overrides config) - #[arg(long, conflicts_with = "no_dry_run")] + #[arg(long, conflicts_with = "dry_run")] pub no_dry_run: bool, } From 6bae6c362d05ae9c87bf984f6ee4c045f39e8150 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 2 Jul 2026 13:13:53 -0400 Subject: [PATCH 02/22] chore: add --version flag and remove -v short from verbose --- src/cli/args.rs | 1 + src/cli/flags.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cli/args.rs b/src/cli/args.rs index ef91b43..6c83994 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -21,6 +21,7 @@ pub enum Command { #[derive(Parser)] #[command( name = "star-setup", + version, about = "Lightweight CLI to clone, configure, and wire single or multi-repo ecosystems", long_about = None, )] diff --git a/src/cli/flags.rs b/src/cli/flags.rs index ec0b85f..095b92f 100644 --- a/src/cli/flags.rs +++ b/src/cli/flags.rs @@ -80,7 +80,7 @@ pub struct MonoRepoFlags { #[derive(ClapArgs)] pub struct DiagnosticFlags { /// Show detailed command output - #[arg(short = 'v', long, conflicts_with = "no_verbose")] + #[arg(long, conflicts_with = "no_verbose")] pub verbose: bool, /// Suppress detailed command output (overrides config) #[arg(long, conflicts_with = "verbose")] From 8a43b09489c2edaabb3e684b504c815177fcde5c Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 2 Jul 2026 13:16:17 -0400 Subject: [PATCH 03/22] fix: --help --build info --- src/cli/flags.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/flags.rs b/src/cli/flags.rs index 095b92f..f0dda44 100644 --- a/src/cli/flags.rs +++ b/src/cli/flags.rs @@ -27,7 +27,7 @@ pub struct BuildFlags { #[arg(long, value_name = "BUILD_SYSTEM")] pub build_system: Option, - // Build after configuring (overrides config) + /// Build after configuring (overrides config) #[arg(long, conflicts_with = "no_build")] pub build: bool, /// Skip building, only configure From 73cfe6591dc59ff68b539a68363c0b88fa0b8e65 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 2 Jul 2026 13:31:25 -0400 Subject: [PATCH 04/22] refactor: add flag help headings and shorten comomon flags --- src/cli/args.rs | 8 ++++---- src/cli/flags.rs | 12 ++++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/cli/args.rs b/src/cli/args.rs index 6c83994..20208cb 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -29,14 +29,14 @@ pub struct Args { /// Repository name (username/repo) or full GitHub URL pub repo: Option, - /// Skip confirmation prompts (non-interactive mode) - #[arg(short = 'y', long)] - pub yes: bool, - /// Select a named configuration to use #[arg(long = "config")] pub config_name: Option, + /// Skip confirmation prompts (non-interactive mode) + #[arg(short = 'y', long)] + pub yes: bool, + #[command(subcommand)] pub command: Option, diff --git a/src/cli/flags.rs b/src/cli/flags.rs index f0dda44..a1c79ed 100644 --- a/src/cli/flags.rs +++ b/src/cli/flags.rs @@ -3,6 +3,7 @@ use clap::Args as ClapArgs; #[allow(clippy::struct_excessive_bools)] #[derive(ClapArgs)] +#[command(next_help_heading = "Connection")] pub struct ConnectionFlags { /// Use SSH instead of HTTPS for cloning #[arg(long, conflicts_with = "https")] @@ -14,9 +15,10 @@ pub struct ConnectionFlags { #[allow(clippy::struct_excessive_bools)] #[derive(ClapArgs)] +#[command(next_help_heading = "Build")] pub struct BuildFlags { /// Build type - #[arg(short = 'b', long)] + #[arg(long)] pub build_type: Option, /// Build directory name @@ -28,7 +30,7 @@ pub struct BuildFlags { pub build_system: Option, /// Build after configuring (overrides config) - #[arg(long, conflicts_with = "no_build")] + #[arg(short = 'b', long, conflicts_with = "no_build")] pub build: bool, /// Skip building, only configure #[arg(short = 'n', long, conflicts_with = "build")] @@ -58,6 +60,7 @@ pub struct BuildFlags { } #[derive(ClapArgs)] +#[command(next_help_heading = "Mono-repo")] pub struct MonoRepoFlags { /// Mono-repo mode #[arg(long)] @@ -72,15 +75,16 @@ pub struct MonoRepoFlags { pub repos: Option>, /// Use saved profile for library repositories - #[arg(long, conflicts_with = "repos")] + #[arg(short = 'p', long, conflicts_with = "repos")] pub profile: Option, } #[allow(clippy::struct_excessive_bools)] #[derive(ClapArgs)] +#[command(next_help_heading = "Diagnostics")] pub struct DiagnosticFlags { /// Show detailed command output - #[arg(long, conflicts_with = "no_verbose")] + #[arg(short = 'v', long, conflicts_with = "no_verbose")] pub verbose: bool, /// Suppress detailed command output (overrides config) #[arg(long, conflicts_with = "verbose")] From cec36817a8c114bf834163f2d3f3c618ee20bbcc Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 2 Jul 2026 13:32:22 -0400 Subject: [PATCH 05/22] chore: bump v0.4.2 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c30351e..9a0ca10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "star-setup" -version = "0.4.1" +version = "0.4.2" edition = "2021" repository = "https://github.com/star-setup/core" description = "Lightweight CLI to clone, configure, and wire single or multi-repo ecosystems" From 43f4ac77aafa70910a978ca3039002b1049c0aac Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 2 Jul 2026 13:58:18 -0400 Subject: [PATCH 06/22] feat: show detect build system message during dry-run --- src/commands/mono/mode.rs | 8 ++++---- src/commands/single.rs | 7 ++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/commands/mono/mode.rs b/src/commands/mono/mode.rs index f9b0e36..8e09428 100644 --- a/src/commands/mono/mode.rs +++ b/src/commands/mono/mode.rs @@ -61,12 +61,12 @@ pub fn mono_repo_mode( let build_system = if let Some(bs) = args.build.build_system { Some(bs) - } else if !ctx.flags.dry_run { - Some(detect_mono_build_system(&repo_dirs, ctx)?) - } else { + } else if ctx.flags.dry_run { + writeln!(ctx.io.output, "Would detect build system after cloning").ok(); None + } else { + Some(detect_mono_build_system(&repo_dirs, ctx)?) }; - let canonical_map = if let Some(bs) = build_system { let map = generate_mono_config(bs, &mono_repo_path, &repos_path, &repo_dirs, &repos, ctx)?; if bs != BuildSystem::Npm { diff --git a/src/commands/single.rs b/src/commands/single.rs index 1a28692..5b553ed 100644 --- a/src/commands/single.rs +++ b/src/commands/single.rs @@ -51,10 +51,11 @@ pub fn single_repo_mode( let build_path = repo_path.join(&args.build.build_dir); let build_system = if let Some(bs) = args.build.build_system { Some(bs) - } else if !ctx.flags.dry_run { - Some(detect_build_system(&repo_path, ctx)?) - } else { + } else if ctx.flags.dry_run { + writeln!(ctx.io.output, "Would detect build system after cloning").ok(); None + } else { + Some(detect_build_system(&repo_path, ctx)?) }; if let Some(build_system) = build_system { From 328400f4a737387faeaa749a639dc2a0f619e466 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 2 Jul 2026 14:09:28 -0400 Subject: [PATCH 07/22] refactor: change dry-run completion message --- src/commands/mono/mode.rs | 6 +++++- src/commands/single.rs | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/commands/mono/mode.rs b/src/commands/mono/mode.rs index 8e09428..1380da8 100644 --- a/src/commands/mono/mode.rs +++ b/src/commands/mono/mode.rs @@ -106,6 +106,10 @@ pub fn mono_repo_mode( ) }; - print_setup_complete(&paths, total, &mut ctx.io, ctx.flags); + if build_system.is_none() && ctx.flags.dry_run { + writeln!(ctx.io.output, "Would finish setup in {}", paths.mono_repo_disp.display()).ok(); + } else { + print_setup_complete(&paths, total, &mut ctx.io, ctx.flags); + } Ok(()) } diff --git a/src/commands/single.rs b/src/commands/single.rs index 5b553ed..50c9c88 100644 --- a/src/commands/single.rs +++ b/src/commands/single.rs @@ -67,7 +67,9 @@ pub fn single_repo_mode( } } - if build_system == Some(BuildSystem::Npm) { + if build_system.is_none() && ctx.flags.dry_run { + writeln!(ctx.io.output, "Would finish in {dir_name}/{}", args.build.build_dir).ok(); + } else if build_system == Some(BuildSystem::Npm) { writeln!(ctx.io.output, "Project finished in {dir_name}").ok(); } else { writeln!( From fc3eaee4c142e35655b22618a76ec7319392f07e Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 2 Jul 2026 14:17:44 -0400 Subject: [PATCH 08/22] fix: config init message dry-run, cargo fmt --- src/commands/mono/mode.rs | 7 ++++++- src/commands/single.rs | 7 ++++++- src/config/crud.rs | 22 +++++++++++++++------- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/commands/mono/mode.rs b/src/commands/mono/mode.rs index 1380da8..c59d9e9 100644 --- a/src/commands/mono/mode.rs +++ b/src/commands/mono/mode.rs @@ -107,7 +107,12 @@ pub fn mono_repo_mode( }; if build_system.is_none() && ctx.flags.dry_run { - writeln!(ctx.io.output, "Would finish setup in {}", paths.mono_repo_disp.display()).ok(); + writeln!( + ctx.io.output, + "Would finish setup in {}", + paths.mono_repo_disp.display() + ) + .ok(); } else { print_setup_complete(&paths, total, &mut ctx.io, ctx.flags); } diff --git a/src/commands/single.rs b/src/commands/single.rs index 50c9c88..1e36968 100644 --- a/src/commands/single.rs +++ b/src/commands/single.rs @@ -68,7 +68,12 @@ pub fn single_repo_mode( } if build_system.is_none() && ctx.flags.dry_run { - writeln!(ctx.io.output, "Would finish in {dir_name}/{}", args.build.build_dir).ok(); + writeln!( + ctx.io.output, + "Would finish in {dir_name}/{}", + args.build.build_dir + ) + .ok(); } else if build_system == Some(BuildSystem::Npm) { writeln!(ctx.io.output, "Project finished in {dir_name}").ok(); } else { diff --git a/src/config/crud.rs b/src/config/crud.rs index 86115fb..7ccf27d 100644 --- a/src/config/crud.rs +++ b/src/config/crud.rs @@ -41,7 +41,14 @@ pub fn create_default_config( return Ok(()); } - if !flags.dry_run { + if flags.dry_run { + writeln!( + io.output, + "Would create config file: {}", + dunce::canonicalize(&path).unwrap_or(path).display() + ) + .ok(); + } else { let mut config = SetupConfig::new(); config.path = Some(path.clone()); config.configs.insert( @@ -62,14 +69,15 @@ pub fn create_default_config( ); save_config(&mut config)?; + + writeln!( + io.output, + "Created config file: {}", + dunce::canonicalize(&path).unwrap_or(path).display() + ) + .ok(); } - writeln!( - io.output, - "Created config file: {}", - dunce::canonicalize(&path).unwrap_or(path).display() - ) - .ok(); writeln!(io.output, "Edit this file to customize your defaults.").ok(); writeln!(io.output, "\nConfig files are checked in this order:").ok(); writeln!(io.output, " 1. ./.star-setup.json (current directory)").ok(); From fe74274cd7adcae88ed5c4470fafa55b72c41b46 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 2 Jul 2026 14:26:15 -0400 Subject: [PATCH 09/22] fix: clarify dry-run overwrite prompt --- src/config/crud.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/config/crud.rs b/src/config/crud.rs index 7ccf27d..71d521c 100644 --- a/src/config/crud.rs +++ b/src/config/crud.rs @@ -31,13 +31,12 @@ pub fn create_default_config( io: &mut IoCtx<'_>, flags: RunFlags, ) -> Result<(), String> { - if path.exists() - && !confirm_abort( - &format!("{} already exists. Overwrite?", path.display()), - yes, - io, - )? - { + let prompt = if flags.dry_run { + format!("{} already exists. Overwrite? [DRY-RUN]: No changes will be made", path.display()) + } else { + format!("{} already exists. Overwrite?", path.display()) + }; + if path.exists() && !confirm_abort(&prompt, yes, io)? { return Ok(()); } From 3235c11d1106e75d4b90f7982f66258ab5230388 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 2 Jul 2026 14:33:43 -0400 Subject: [PATCH 10/22] fix: npm workspace clean removes node_modules, clarify dry-run overwrite prompt --- src/config/crud.rs | 5 ++++- src/workspace/clean.rs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/config/crud.rs b/src/config/crud.rs index 71d521c..c3ec845 100644 --- a/src/config/crud.rs +++ b/src/config/crud.rs @@ -32,7 +32,10 @@ pub fn create_default_config( flags: RunFlags, ) -> Result<(), String> { let prompt = if flags.dry_run { - format!("{} already exists. Overwrite? [DRY-RUN]: No changes will be made", path.display()) + format!( + "{} already exists. Overwrite? [DRY-RUN]: No changes will be made", + path.display() + ) } else { format!("{} already exists. Overwrite?", path.display()) }; diff --git a/src/workspace/clean.rs b/src/workspace/clean.rs index cf85f54..1874e5d 100644 --- a/src/workspace/clean.rs +++ b/src/workspace/clean.rs @@ -6,6 +6,38 @@ impl Workspace { /// # Errors /// Returns an error if the build directory cannot be removed. pub fn clean(&self, ctx: &mut RunCtx<'_, '_>) -> Result<(), String> { + if self.root.join("package.json").exists() { + let node_modules = self.root.join("node_modules"); + if !node_modules.exists() { + writeln!( + ctx.io.output, + "node_modules does not exist: {}", + node_modules.display() + ) + .ok(); + return Ok(()); + } + if ctx.flags.dry_run { + writeln!( + ctx.io.output, + "Would remove directory: {}", + node_modules.display() + ) + .ok(); + } else { + writeln!( + ctx.io.output, + "Removing node_modules: {}", + node_modules.display() + ) + .ok(); + fs::remove_dir_all(&node_modules) + .map_err(|e| format!("Failed to remove node_modules: {e}"))?; + writeln!(ctx.io.output, "Done").ok(); + } + return Ok(()); + } + if !self.build_path.exists() { writeln!( ctx.io.output, From d35ce34d3f8e5ba60941cc2a136cbd0e33fb9972 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 2 Jul 2026 21:44:21 -0400 Subject: [PATCH 11/22] fix: timing and verbose logging fixes --- src/cli/build/detect.rs | 24 +++++++- src/commands/build.rs | 6 +- src/commands/handlers.rs | 26 ++++++-- src/commands/header.rs | 4 ++ src/commands/mono/clone.rs | 22 ++++--- src/commands/mono/config.rs | 4 +- src/commands/mono/display.rs | 10 +-- src/commands/mono/mode.rs | 55 ++++++++++++----- src/commands/mono/resolve.rs | 33 +--------- src/commands/mono/setup.rs | 2 +- src/commands/mono/watch.rs | 69 ++++++++++++--------- src/commands/setup.rs | 37 +++++++----- src/commands/single.rs | 45 +++++++------- src/config/crud.rs | 48 +++++++-------- src/config/display.rs | 3 +- src/config/io.rs | 91 ++++++++++++++++++---------- src/ctx.rs | 4 +- src/interactive.rs | 27 +++++---- src/profile/crud.rs | 28 +++++---- src/prompts.rs | 4 +- src/repository.rs | 39 +++++++----- src/run.rs | 9 ++- src/utils/prerequisites.rs | 17 +++++- src/utils/process.rs | 4 +- src/utils/timing.rs | 5 +- src/workspace/clean.rs | 30 +++++---- src/workspace/resolve.rs | 8 ++- src/workspace/status.rs | 107 ++++++++++++++++++--------------- src/workspace/update.rs | 35 ++++++----- tests/cli/build/detect.rs | 4 +- tests/commands/header.rs | 1 + tests/commands/mono/resolve.rs | 10 +-- tests/config/crud.rs | 2 +- tests/config/io.rs | 31 +++++----- tests/ctx.rs | 4 +- tests/profile/crud.rs | 7 ++- tests/repository.rs | 4 +- tests/workspace/resolve.rs | 28 ++++----- 38 files changed, 516 insertions(+), 371 deletions(-) diff --git a/src/cli/build/detect.rs b/src/cli/build/detect.rs index 0318a84..555a8ce 100644 --- a/src/cli/build/detect.rs +++ b/src/cli/build/detect.rs @@ -28,15 +28,24 @@ fn pick_build_system( /// # Errors /// Returns an error on EOF during prompt, or if no supported build system is found. pub fn detect_build_system(dir: &Path, ctx: &mut RunCtx<'_, '_>) -> Result { - crate::time!(ctx.flags.timing, ctx.io.output, "Detect", { + crate::time!(ctx.flags.timing, ctx.io.output, "Scanned directory", { let mut detected = Vec::new(); if dir.join("CMakeLists.txt").exists() { + if ctx.flags.verbose { + writeln!(ctx.io.output, " Found CMakeLists.txt").ok(); + } detected.push(BuildSystem::Cmake); } if dir.join("meson.build").exists() { + if ctx.flags.verbose { + writeln!(ctx.io.output, " Found meson.build").ok(); + } detected.push(BuildSystem::Meson); } if dir.join("package.json").exists() { + if ctx.flags.verbose { + writeln!(ctx.io.output, " Found package.json").ok(); + } detected.push(BuildSystem::Npm); } pick_build_system(&detected, "No supported build system found", ctx) @@ -50,16 +59,25 @@ pub fn detect_mono_build_system( dirs: &[PathBuf], ctx: &mut RunCtx<'_, '_>, ) -> Result { - writeln!(ctx.io.output, "Detecting build system\n").ok(); - crate::time!(ctx.flags.timing, ctx.io.output, "Detect", { + writeln!(ctx.io.output, " Detecting build system\n").ok(); + crate::time!(ctx.flags.timing, ctx.io.output, "Scanned directories", { let mut detected = Vec::new(); if dirs.iter().all(|d| d.join("CMakeLists.txt").exists()) { + if ctx.flags.verbose { + writeln!(ctx.io.output, " Found CMakeLists.txt").ok(); + } detected.push(BuildSystem::Cmake); } if dirs.iter().all(|d| d.join("meson.build").exists()) { + if ctx.flags.verbose { + writeln!(ctx.io.output, " Found meson.build").ok(); + } detected.push(BuildSystem::Meson); } if dirs.iter().all(|d| d.join("package.json").exists()) { + if ctx.flags.verbose { + writeln!(ctx.io.output, " Found package.json").ok(); + } detected.push(BuildSystem::Npm); } pick_build_system( diff --git a/src/commands/build.rs b/src/commands/build.rs index 9578d71..cb75317 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -30,7 +30,7 @@ pub fn cmake_build( }); if !args.build.no_build { - writeln!(ctx.io.output, "Building project\n").ok(); + writeln!(ctx.io.output, "Building project").ok(); crate::time!(ctx.flags.timing, ctx.io.output, "CMake build", { ctx.runner.run( &[ @@ -75,7 +75,7 @@ pub fn meson_build( ctx.runner.run(&meson_cmd, None, ctx.flags, ctx.io.output)?; }); if !args.build.no_build { - writeln!(ctx.io.output, "Building project\n").ok(); + writeln!(ctx.io.output, "Building project").ok(); crate::time!(ctx.flags.timing, ctx.io.output, "Meson compile", { ctx.runner.run( &["meson", "compile", "-C", to_str(build_path)?], @@ -106,7 +106,7 @@ pub fn npm_build( )?; }); if !args.build.no_build && !is_mono { - writeln!(ctx.io.output, "Building project\n").ok(); + writeln!(ctx.io.output, "Building project").ok(); crate::time!(ctx.flags.timing, ctx.io.output, "npm build", { ctx.runner.run( &["npm", "run", "build"], diff --git a/src/commands/handlers.rs b/src/commands/handlers.rs index a00753e..17f2e2a 100644 --- a/src/commands/handlers.rs +++ b/src/commands/handlers.rs @@ -64,7 +64,7 @@ pub fn handle_profile_cmd( /// Returns an error if resolving, updating, cleaning, or fetching status for the workspace fails. pub fn handle_workspace_cmd( action: WorkspaceAction, - io: IoCtx, + mut io: IoCtx, flags: RunFlags, ) -> Result<(), Box> { match action { @@ -73,7 +73,13 @@ pub fn handle_workspace_cmd( mono_dir, build_dir, } => { - let ws = resolve_workspace(path.as_deref(), mono_dir.as_deref(), build_dir.as_deref())?; + let ws = resolve_workspace( + path.as_deref(), + mono_dir.as_deref(), + build_dir.as_deref(), + &mut io, + flags.verbose, + )?; with_runner(io, flags, |ctx| ws.update(ctx).map_err(Into::into))?; } WorkspaceAction::Status { @@ -82,7 +88,13 @@ pub fn handle_workspace_cmd( build_dir, fetch, } => { - let ws = resolve_workspace(path.as_deref(), mono_dir.as_deref(), build_dir.as_deref())?; + let ws = resolve_workspace( + path.as_deref(), + mono_dir.as_deref(), + build_dir.as_deref(), + &mut io, + flags.verbose, + )?; with_runner(io, flags, |ctx| ws.status(fetch, ctx).map_err(Into::into))?; } WorkspaceAction::Clean { @@ -90,7 +102,13 @@ pub fn handle_workspace_cmd( mono_dir, build_dir, } => { - let ws = resolve_workspace(path.as_deref(), mono_dir.as_deref(), build_dir.as_deref())?; + let ws = resolve_workspace( + path.as_deref(), + mono_dir.as_deref(), + build_dir.as_deref(), + &mut io, + flags.verbose, + )?; with_runner(io, flags, |ctx| ws.clean(ctx).map_err(Into::into))?; } } diff --git a/src/commands/header.rs b/src/commands/header.rs index 5d285c5..3f8b4fd 100644 --- a/src/commands/header.rs +++ b/src/commands/header.rs @@ -11,6 +11,7 @@ pub struct ModeHeader<'a> { pub mono_dir: Option<&'a str>, pub profile: Option<&'a str>, pub lib_count: Option, + pub repo_count: Option, } /// Prints a formatted header summarizing the current mode and configuration. @@ -36,5 +37,8 @@ pub fn print_mode_header(header: &ModeHeader<'_>, io: &mut IoCtx<'_>) { if let Some(c) = header.lib_count { writeln!(io.output, " Libraries: {c}").ok(); } + if let Some(c) = header.repo_count { + writeln!(io.output, " Total repositories: {c}").ok(); + } writeln!(io.output).ok(); } diff --git a/src/commands/mono/clone.rs b/src/commands/mono/clone.rs index 2c759f1..a4cd2db 100644 --- a/src/commands/mono/clone.rs +++ b/src/commands/mono/clone.rs @@ -12,15 +12,23 @@ pub fn clone_mono_repos( writeln!(ctx.io.output, "Cloning repositories").ok(); crate::time!(ctx.flags.timing, ctx.io.output, "Clone", { for repo in repos { - clone_repository(repo, repos_path, ssh, ctx)?; + writeln!( + ctx.io.output, + " Cloning {}", + crate::repository::repo_dir_name(repo) + ) + .ok(); + clone_repository(repo, repos_path, ssh, true, false, ctx)?; } + writeln!( + ctx.io.output, + " Finished cloning ({} repositories)", + repos.len() + ) + .ok(); Ok::<(), String>(()) })?; - writeln!( - ctx.io.output, - "\n Finished cloning ({} repositories)\n", - repos.len() - ) - .ok(); + + writeln!(ctx.io.output).ok(); Ok(()) } diff --git a/src/commands/mono/config.rs b/src/commands/mono/config.rs index 11e9b6a..0487be9 100644 --- a/src/commands/mono/config.rs +++ b/src/commands/mono/config.rs @@ -27,7 +27,7 @@ fn write_mono_repo_config( #[allow(clippy::to_string_in_format_args)] writeln!( io.output, - "Created root {} at {}\n", + " Created root {} at {}\n", filename.to_string(), mono_dir.display() ) @@ -194,7 +194,7 @@ pub fn create_mono_repo_package_json( #[allow(clippy::to_string_in_format_args)] writeln!( io.output, - "Created root {} at {}\n", + " Created root {} at {}\n", "package.json".to_string(), mono_dir.display() ) diff --git a/src/commands/mono/display.rs b/src/commands/mono/display.rs index a37304e..69aa62e 100644 --- a/src/commands/mono/display.rs +++ b/src/commands/mono/display.rs @@ -71,20 +71,20 @@ pub fn print_setup_complete( io: &mut IoCtx<'_>, flags: RunFlags, ) { - writeln!(io.output, "Setup complete").ok(); + writeln!(io.output, " Setup complete").ok(); writeln!( io.output, - "Repositories in: {}", + " Repositories in: {}", paths.mono_repo_disp.display() ) .ok(); if let Some(exe) = &paths.exe_path { - writeln!(io.output, "Executable: {}", exe.display()).ok(); + writeln!(io.output, " Executable: {}", exe.display()).ok(); } if let Some(build) = &paths.build_disp { - writeln!(io.output, "Build output in: {}", build.display()).ok(); + writeln!(io.output, " Build output in: {}", build.display()).ok(); } if flags.timing { - writeln!(io.output, "[timing] Total: {:.2?}", total.elapsed()).ok(); + writeln!(io.output, " [timing] Total: {:.2?}", total.elapsed()).ok(); } } diff --git a/src/commands/mono/mode.rs b/src/commands/mono/mode.rs index c59d9e9..e04716d 100644 --- a/src/commands/mono/mode.rs +++ b/src/commands/mono/mode.rs @@ -7,7 +7,7 @@ use crate::{ display::{resolve_setup_paths, SetupPaths}, generate_mono_config, generate_watch_scripts, open_watch_scripts, print_setup_complete, }, - prepare_build_dir, resolve_repos_for_mono, resolve_test_repo, + prepare_build_dir, print_mode_header, resolve_repos_for_mono, resolve_test_repo, ModeHeader, }, config::SetupConfig, ctx::RunCtx, @@ -28,19 +28,36 @@ pub fn mono_repo_mode( ctx: &mut RunCtx<'_, '_>, ) -> Result<(), String> { let total = std::time::Instant::now(); - let repo_input = extract_repo_input(args)?; let test_repo = resolve_test_repo(repo_input)?; - let deps = resolve_repos_for_mono(args, config, &test_repo, &mut ctx.io)?; + let deps = resolve_repos_for_mono(args, config, &mut ctx.io)?; let repos = build_repo_list(&test_repo, &deps); - writeln!(ctx.io.output, "Total repositories: {}\n", repos.len()).ok(); + print_mode_header( + &ModeHeader { + mode: if args.mono.profile.is_some() { + "Profile" + } else { + "Mono-repository" + }, + test_repo: Some(&test_repo), + repo_name: None, + use_ssh: args.connection.ssh, + mono_dir: Some(&args.mono.mono_dir), + profile: args.mono.profile.as_deref(), + lib_count: Some(deps.len()), + repo_count: Some(repos.len()), + }, + &mut ctx.io, + ); + + writeln!(ctx.io.output, "Creating repo folder").ok(); let mono_repo_path = base_dir.join(&args.mono.mono_dir); let repos_path = mono_repo_path.join("repos"); if ctx.flags.dry_run { writeln!( ctx.io.output, - "Would create directory: {}", + " Would create directory: {}", repos_path.display() ) .ok(); @@ -49,6 +66,7 @@ pub fn mono_repo_mode( fs::create_dir_all(&repos_path).map_err(|e| e.to_string())?; }); } + writeln!(ctx.io.output, " Finished creating\n").ok(); clone_mono_repos(&repos, &repos_path, args.connection.ssh, ctx)?; @@ -57,16 +75,25 @@ pub fn mono_repo_mode( .map(|r| repos_path.join(repo_dir_name(r))) .collect(); + writeln!(ctx.io.output, "Detecting build system").ok(); let build_path = mono_repo_path.join(&args.build.build_dir); + let build_system = crate::time!(ctx.flags.timing, ctx.io.output, "Detect", { + let result = if let Some(bs) = args.build.build_system { + if ctx.flags.verbose { + writeln!(ctx.io.output, " Build system flag set: {bs:?}").ok(); + } + Some(bs) + } else if ctx.flags.dry_run { + writeln!(ctx.io.output, " Would detect build system after cloning").ok(); + None + } else { + Some(detect_mono_build_system(&repo_dirs, ctx)?) + }; + writeln!(ctx.io.output, " Finished detecting").ok(); + result + }); + writeln!(ctx.io.output).ok(); - let build_system = if let Some(bs) = args.build.build_system { - Some(bs) - } else if ctx.flags.dry_run { - writeln!(ctx.io.output, "Would detect build system after cloning").ok(); - None - } else { - Some(detect_mono_build_system(&repo_dirs, ctx)?) - }; let canonical_map = if let Some(bs) = build_system { let map = generate_mono_config(bs, &mono_repo_path, &repos_path, &repo_dirs, &repos, ctx)?; if bs != BuildSystem::Npm { @@ -82,7 +109,7 @@ pub fn mono_repo_mode( if build_system == Some(BuildSystem::Npm) && !args.build.no_watch && !ctx.flags.dry_run { generate_watch_scripts(&mono_repo_path, &repos_path, &repos, &mut ctx.io, ctx.flags)?; if args.build.watch { - open_watch_scripts(&mono_repo_path, &mut ctx.io)?; + open_watch_scripts(&mono_repo_path, &mut ctx.io, ctx.flags)?; } } diff --git a/src/commands/mono/resolve.rs b/src/commands/mono/resolve.rs index 665b26b..17ae900 100644 --- a/src/commands/mono/resolve.rs +++ b/src/commands/mono/resolve.rs @@ -1,10 +1,4 @@ -use crate::{ - cli::ResolvedArgs, - commands::{print_mode_header, ModeHeader}, - config::SetupConfig, - ctx::IoCtx, - profile::list_profiles, -}; +use crate::{cli::ResolvedArgs, config::SetupConfig, ctx::IoCtx, profile::list_profiles}; /// Resolves the list of repositories for mono-repo mode from a profile or explicit repo list. /// # Errors @@ -12,7 +6,6 @@ use crate::{ pub fn resolve_repos_for_mono( args: &ResolvedArgs, config: &SetupConfig, - test_repo: &str, io: &mut IoCtx<'_>, ) -> Result, String> { if let Some(profile_name) = &args.mono.profile { @@ -23,32 +16,8 @@ pub fn resolve_repos_for_mono( if profile_repos.is_empty() { return Err(format!("Profile '{profile_name}' has no repositories")); } - print_mode_header( - &ModeHeader { - mode: "Profile", - test_repo: Some(test_repo), - repo_name: None, - use_ssh: args.connection.ssh, - mono_dir: Some(&args.mono.mono_dir), - profile: Some(profile_name), - lib_count: Some(profile_repos.len()), - }, - io, - ); Ok(profile_repos.clone()) } else if let Some(r) = &args.mono.repos { - print_mode_header( - &ModeHeader { - mode: "Mono-repository", - test_repo: Some(test_repo), - repo_name: None, - use_ssh: args.connection.ssh, - mono_dir: Some(&args.mono.mono_dir), - profile: None, - lib_count: Some(r.len()), - }, - io, - ); Ok(r.clone()) } else { Err("No repos or profile specified for mono-repo mode".to_string()) diff --git a/src/commands/mono/setup.rs b/src/commands/mono/setup.rs index 52c8c31..e409549 100644 --- a/src/commands/mono/setup.rs +++ b/src/commands/mono/setup.rs @@ -20,7 +20,7 @@ pub fn generate_mono_config( repos: &[String], ctx: &mut RunCtx<'_, '_>, ) -> Result>, String> { - writeln!(ctx.io.output, "Creating mono-repo configuration").ok(); + writeln!(ctx.io.output, " Creating mono-repo configuration").ok(); match build_system { BuildSystem::Cmake => { create_mono_repo_cmakelists(mono_repo_path, repos, &mut ctx.io, ctx.flags)?; diff --git a/src/commands/mono/watch.rs b/src/commands/mono/watch.rs index e79b77a..0cadc39 100644 --- a/src/commands/mono/watch.rs +++ b/src/commands/mono/watch.rs @@ -97,20 +97,23 @@ pub fn generate_watch_scripts( sh_lines.join("\n") ); - fs::write(mono_dir.join("watch.ps1"), ps1_content) - .map_err(|e| format!("Failed to write watch.ps1: {e}"))?; - fs::write(mono_dir.join("watch.sh"), sh_content) - .map_err(|e| format!("Failed to write watch.sh: {e}"))?; + crate::time!(flags.timing, io.output, "Write scripts", { + fs::write(mono_dir.join("watch.ps1"), ps1_content) + .map_err(|e| format!("Failed to write watch.ps1: {e}"))?; + fs::write(mono_dir.join("watch.sh"), sh_content) + .map_err(|e| format!("Failed to write watch.sh: {e}"))?; + Ok::<(), String>(()) + })?; writeln!( io.output, - "Generated watch scripts at {}", + " Generated watch scripts at {}", mono_dir.display() ) .ok(); if flags.verbose { - writeln!(io.output, "Watching {} libraries:", lib_dirs.len()).ok(); + writeln!(io.output, " Watching {} libraries:", lib_dirs.len()).ok(); for d in &lib_dirs { let full_path = dunce::canonicalize(repos_path.join(d)).unwrap_or_else(|_| repos_path.join(d)); @@ -124,30 +127,36 @@ pub fn generate_watch_scripts( /// Opens watch scripts in new terminals. /// # Errors /// Returns an error if the terminal cannot be opened. -pub fn open_watch_scripts(mono_dir: &Path, io: &mut IoCtx<'_>) -> Result<(), String> { - #[cfg(target_os = "windows")] - { - let ps1_path = mono_dir.join("watch.ps1"); - std::process::Command::new("powershell") - .args([ - "-ExecutionPolicy", - "Bypass", - "-File", - ps1_path.to_str().ok_or("Invalid path")?, - ]) - .spawn() - .map_err(|e| format!("Failed to open watch.ps1: {e}"))?; - } - - #[cfg(not(target_os = "windows"))] - { - let sh_path = mono_dir.join("watch.sh"); - std::process::Command::new("bash") - .arg(sh_path.to_str().ok_or("Invalid path")?) - .spawn() - .map_err(|e| format!("Failed to open watch.sh: {e}"))?; - } +pub fn open_watch_scripts( + mono_dir: &Path, + io: &mut IoCtx<'_>, + flags: RunFlags, +) -> Result<(), String> { + crate::time!(flags.timing, io.output, "Open", { + #[cfg(target_os = "windows")] + { + let ps1_path = mono_dir.join("watch.ps1"); + std::process::Command::new("powershell") + .args([ + "-ExecutionPolicy", + "Bypass", + "-File", + ps1_path.to_str().ok_or("Invalid path")?, + ]) + .spawn() + .map_err(|e| format!("Failed to open watch.ps1: {e}"))?; + } - writeln!(io.output, "Opening watch scripts").ok(); + #[cfg(not(target_os = "windows"))] + { + let sh_path = mono_dir.join("watch.sh"); + std::process::Command::new("bash") + .arg(sh_path.to_str().ok_or("Invalid path")?) + .spawn() + .map_err(|e| format!("Failed to open watch.sh: {e}"))?; + } + Ok::<(), String>(()) + })?; + writeln!(io.output, " Opening watch scripts").ok(); Ok(()) } diff --git a/src/commands/setup.rs b/src/commands/setup.rs index e83ffca..293ee7c 100644 --- a/src/commands/setup.rs +++ b/src/commands/setup.rs @@ -13,26 +13,29 @@ pub fn prepare_build_dir( clean: bool, ctx: &mut RunCtx<'_, '_>, ) -> Result<(), String> { - if clean && ctx.flags.dry_run { - writeln!(ctx.io.output, "Cleaning build directory\n").ok(); - writeln!( - ctx.io.output, - "Would remove directory: {}", - build_path.display() - ) - .ok(); - } else if clean && build_path.exists() { - writeln!(ctx.io.output, "Cleaning build directory\n").ok(); - crate::time!(ctx.flags.timing, ctx.io.output, "Clean", { - fs::remove_dir_all(build_path).map_err(|e| e.to_string())?; - }); + if clean { + writeln!(ctx.io.output, "Cleaning build directory").ok(); + if ctx.flags.dry_run { + writeln!( + ctx.io.output, + " Would remove directory: {}", + build_path.display() + ) + .ok(); + } else if build_path.exists() { + crate::time!(ctx.flags.timing, ctx.io.output, "Clean", { + fs::remove_dir_all(build_path).map_err(|e| e.to_string())?; + }); + } + writeln!(ctx.io.output, " Finished cleaning").ok(); + writeln!(ctx.io.output).ok(); } - writeln!(ctx.io.output, "Creating build directory\n").ok(); + writeln!(ctx.io.output, "Creating build directory").ok(); if ctx.flags.dry_run { writeln!( ctx.io.output, - "Would create directory: {}", + " Would create directory: {}", build_path.display() ) .ok(); @@ -41,6 +44,8 @@ pub fn prepare_build_dir( fs::create_dir_all(build_path).map_err(|e| e.to_string())?; }); } + writeln!(ctx.io.output, " Finished creating").ok(); + writeln!(ctx.io.output).ok(); Ok(()) } @@ -55,7 +60,7 @@ pub fn configure_and_build( is_mono: bool, ctx: &mut RunCtx<'_, '_>, ) -> Result<(), String> { - writeln!(ctx.io.output, "Configuring project\n").ok(); + writeln!(ctx.io.output, "Configuring project").ok(); build_project(args, build_path, project_path, build_system, is_mono, ctx) } diff --git a/src/commands/single.rs b/src/commands/single.rs index 1e36968..51f0fd9 100644 --- a/src/commands/single.rs +++ b/src/commands/single.rs @@ -4,8 +4,7 @@ use crate::{ configure_and_build, extract_repo_input, prepare_build_dir, print_mode_header, ModeHeader, }, ctx::RunCtx, - prompts::confirm, - repository::{clone_repository, pull_repository, repo_dir_name}, + repository::{clone_repository, repo_dir_name}, }; use std::path::Path; @@ -18,9 +17,9 @@ pub fn single_repo_mode( ctx: &mut RunCtx<'_, '_>, ) -> Result<(), String> { let total = std::time::Instant::now(); - let repo = extract_repo_input(args)?; let dir_name = repo_dir_name(repo); + let repo_path = base_dir.join(&dir_name); print_mode_header( &ModeHeader { @@ -31,32 +30,32 @@ pub fn single_repo_mode( mono_dir: None, profile: None, lib_count: None, + repo_count: None, }, &mut ctx.io, ); - let repo_path = base_dir.join(&dir_name); - if repo_path.exists() { - writeln!(ctx.io.output, "Repository {dir_name} already exists").ok(); - if confirm("Update existing repository?", args.yes, &mut ctx.io)? { - writeln!(ctx.io.output, "Updating {dir_name}\n").ok(); - crate::time!(ctx.flags.timing, ctx.io.output, "Update", { - pull_repository(&repo_path, ctx)?; - }); - } - } else { - clone_repository(repo, base_dir, args.connection.ssh, ctx)?; - } + writeln!(ctx.io.output, "Cloning repository").ok(); + clone_repository(repo, base_dir, args.connection.ssh, false, args.yes, ctx)?; + writeln!(ctx.io.output, "Detecting build system").ok(); let build_path = repo_path.join(&args.build.build_dir); - let build_system = if let Some(bs) = args.build.build_system { - Some(bs) - } else if ctx.flags.dry_run { - writeln!(ctx.io.output, "Would detect build system after cloning").ok(); - None - } else { - Some(detect_build_system(&repo_path, ctx)?) - }; + let build_system = crate::time!(ctx.flags.timing, ctx.io.output, "Detect", { + let result = if let Some(bs) = args.build.build_system { + if ctx.flags.verbose { + writeln!(ctx.io.output, " Build system flag set: {bs:?}").ok(); + } + Some(bs) + } else if ctx.flags.dry_run { + writeln!(ctx.io.output, " Would detect build system after cloning").ok(); + None + } else { + Some(detect_build_system(&repo_path, ctx)?) + }; + writeln!(ctx.io.output, " Finished detecting").ok(); + result + }); + writeln!(ctx.io.output).ok(); if let Some(build_system) = build_system { if build_system == BuildSystem::Npm { diff --git a/src/config/crud.rs b/src/config/crud.rs index c3ec845..4f385a9 100644 --- a/src/config/crud.rs +++ b/src/config/crud.rs @@ -33,11 +33,11 @@ pub fn create_default_config( ) -> Result<(), String> { let prompt = if flags.dry_run { format!( - "{} already exists. Overwrite? [DRY-RUN]: No changes will be made", + " {} already exists. Overwrite? [DRY-RUN]: No changes will be made", path.display() ) } else { - format!("{} already exists. Overwrite?", path.display()) + format!(" {} already exists. Overwrite?", path.display()) }; if path.exists() && !confirm_abort(&prompt, yes, io)? { return Ok(()); @@ -46,7 +46,7 @@ pub fn create_default_config( if flags.dry_run { writeln!( io.output, - "Would create config file: {}", + " Would create config file: {}", dunce::canonicalize(&path).unwrap_or(path).display() ) .ok(); @@ -70,20 +70,20 @@ pub fn create_default_config( }, ); - save_config(&mut config)?; + let path = save_config(&mut config, flags.timing, &mut io.output)?; writeln!( io.output, - "Created config file: {}", + " Created config file: {}", dunce::canonicalize(&path).unwrap_or(path).display() ) .ok(); } - writeln!(io.output, "Edit this file to customize your defaults.").ok(); - writeln!(io.output, "\nConfig files are checked in this order:").ok(); - writeln!(io.output, " 1. ./.star-setup.json (current directory)").ok(); - writeln!(io.output, " 2. ~/.star-setup.json (home directory)").ok(); + writeln!(io.output, " Edit this file to customize your defaults.\n").ok(); + writeln!(io.output, " Config files are checked in this order:").ok(); + writeln!(io.output, " 1. ./.star-setup.json (current directory)").ok(); + writeln!(io.output, " 2. ~/.star-setup.json (home directory)").ok(); Ok(()) } @@ -101,7 +101,7 @@ pub fn add_config( ) -> Result<(), String> { if has_config(config, name) && !confirm_abort( - &format!("Warning: Configuration '{name}' already exists. Overwrite?"), + &format!(" Warning: Configuration '{name}' already exists. Overwrite?"), yes, io, )? @@ -112,21 +112,21 @@ pub fn add_config( if flags.dry_run { writeln!( io.output, - "Would save configuration '{name}' to config file" + " Would save configuration '{name}' to config file" ) .ok(); } else { insert_config(config, name, entry); - let path = save_config(config)?; + let path: PathBuf = save_config(config, flags.timing, &mut io.output)?; writeln!( io.output, - "Configuration '{name}' added successfully to {}", + " 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(); + writeln!(io.output, " Configuration details:").ok(); + write!(io.output, " {}", format_entry(e)).ok(); } Ok(()) } @@ -142,29 +142,29 @@ pub fn remove_config( flags: RunFlags, ) -> Result<(), String> { let Some(e) = config.configs.get(name) else { - writeln!(io.output, "\nWarning: Config '{name}' not found.\n").ok(); + writeln!(io.output, " Warning: Config '{name}' not found.\n").ok(); return Ok(()); }; - writeln!(io.output, "Config {name}").ok(); - writeln!(io.output, "Configuration details:").ok(); - write!(io.output, "{}", format_entry(e)).ok(); + writeln!(io.output, " Config {name}").ok(); + writeln!(io.output, " Configuration details:").ok(); + write!(io.output, " {}", format_entry(e)).ok(); - if !confirm_abort("\nAre you sure you want to remove this config?", yes, io)? { + if !confirm_abort(" Are you sure you want to remove this config?", yes, io)? { return Ok(()); } if flags.dry_run { writeln!( io.output, - "Would remove configuration '{name}' from config file" + " 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(); + let path = save_config(config, flags.timing, &mut io.output)?; + writeln!(io.output, " Config '{name}' was successfully removed").ok(); + writeln!(io.output, " Configuration saved to: {}", path.display()).ok(); } Ok(()) } diff --git a/src/config/display.rs b/src/config/display.rs index 45d0ba1..f64ecc7 100644 --- a/src/config/display.rs +++ b/src/config/display.rs @@ -53,7 +53,8 @@ pub fn list_configs(config: &SetupConfig, io: &mut IoCtx<'_>) { writeln!(io.output, "Configurations:").ok(); for (name, e) in &config.configs { - writeln!(io.output, "\n{name}:").ok(); + writeln!(io.output, "{name}:").ok(); write!(io.output, "{}", format_entry(e)).ok(); + writeln!(io.output).ok(); } } diff --git a/src/config/io.rs b/src/config/io.rs index e6c5eca..01b736f 100644 --- a/src/config/io.rs +++ b/src/config/io.rs @@ -14,44 +14,64 @@ pub fn config_locations(path: &std::path::Path) -> Vec { } /// Loads configuration from the first valid JSON file in `locations`. -pub fn load_config(locations: &[PathBuf], output: &mut impl Write) -> SetupConfig { +pub fn load_config( + locations: &[PathBuf], + verbose: bool, + timing: bool, + output: &mut impl Write, +) -> SetupConfig { let mut invalid_count = 0; for path in locations { if !path.exists() { + if verbose { + writeln!(output, " Config path not found: {}", path.display()).ok(); + } continue; } - match fs::read_to_string(path) { - Ok(contents) => match serde_json::from_str::(&contents) { - Ok(mut config) => { - config.path = Some(path.clone()); - return config; + let result = crate::time!(timing, output, "Read config", { + match fs::read_to_string(path) { + Ok(contents) => match serde_json::from_str::(&contents) { + Ok(mut config) => { + config.path = Some(path.clone()); + if verbose { + writeln!(output, " Loaded config from {}", path.display()).ok(); + } + Some(config) + } + Err(e) => { + writeln!(output, " Warning: Invalid JSON in {}: {e}", path.display()).ok(); + invalid_count += 1; + None + } + }, + Err(e) if e.kind() == io::ErrorKind::PermissionDenied => { + writeln!(output, " Error: No permission to read {}", path.display()).ok(); + invalid_count += 1; + None } Err(e) => { - writeln!(output, "Warning: Invalid JSON in {}: {e}", path.display()).ok(); + writeln!( + output, + " An unexpected error occurred reading {}: {e}", + path.display() + ) + .ok(); invalid_count += 1; + None } - }, - Err(e) if e.kind() == io::ErrorKind::PermissionDenied => { - writeln!(output, "Error: No permission to read {}", path.display()).ok(); - invalid_count += 1; - } - Err(e) => { - writeln!( - output, - "An unexpected error occurred reading {}: {e}", - path.display() - ) - .ok(); - invalid_count += 1; } + }); + + if let Some(config) = result { + return config; } } if invalid_count != 0 { writeln!( output, - "Found {invalid_count} config file{} that had errors", + " Found {invalid_count} config file{} that had errors", if invalid_count == 1 { "" } else { "s" } ) .ok(); @@ -62,7 +82,11 @@ pub fn load_config(locations: &[PathBuf], output: &mut impl Write) -> SetupConfi /// Serializes the configuration and writes it to the path stored in `config.path`. /// # Errors /// Returns an error if serialization fails or if the file cannot be written. -pub fn save_config(config: &mut SetupConfig) -> Result { +pub fn save_config( + config: &mut SetupConfig, + timing: bool, + output: &mut impl Write, +) -> Result { let path = config .path .get_or_insert_with(|| { @@ -75,15 +99,18 @@ pub fn save_config(config: &mut SetupConfig) -> Result { let json = serde_json::to_string_pretty(config).map_err(|e| format!("Failed to serialize config: {e}"))?; - fs::write(&path, json).map_err(|e| match e.kind() { - io::ErrorKind::PermissionDenied => { - format!("Error: No permission to write to {}", path.display()) - } - _ => format!( - "An unexpected error occurred writing {}: {}", - path.display(), - e - ), - })?; + crate::time!(timing, output, "Write config", { + fs::write(&path, json).map_err(|e| match e.kind() { + io::ErrorKind::PermissionDenied => { + format!("Error: No permission to write to {}", path.display()) + } + _ => format!( + "An unexpected error occurred writing {}: {}", + path.display(), + e + ), + })?; + }); + Ok(path) } diff --git a/src/ctx.rs b/src/ctx.rs index a8937ed..4c9683d 100644 --- a/src/ctx.rs +++ b/src/ctx.rs @@ -86,9 +86,9 @@ impl Runner for DryRunRunner { _flags: RunFlags, output: &mut dyn Write, ) -> Result<(), String> { - writeln!(output, "Would run: {}", cmd.join(" ")).map_err(|e| e.to_string())?; + writeln!(output, " Would run: {}", cmd.join(" ")).map_err(|e| e.to_string())?; if let Some(dir) = cwd { - writeln!(output, " in directory: {}", dir.display()).map_err(|e| e.to_string())?; + writeln!(output, " in directory: {}", dir.display()).map_err(|e| e.to_string())?; } Ok(()) } diff --git a/src/interactive.rs b/src/interactive.rs index 0ed522b..331f7b4 100644 --- a/src/interactive.rs +++ b/src/interactive.rs @@ -13,17 +13,17 @@ pub fn interactive_mode(args: &mut ResolvedArgs, io: &mut IoCtx<'_>) -> Result<( writeln!(io.output, "Star Setup Interactive Mode").ok(); if args.repo.is_none() { - args.repo = Some(ask_required("Enter repository (user/repo or URL)", io)?); + args.repo = Some(ask_required(" Enter repository (user/repo or URL)", io)?); } - args.connection.ssh = ask_bool_if("Use SSH?", args.connection.ssh, io)?; - args.diagnostic.verbose = ask_bool_if("Verbose?", args.diagnostic.verbose, io)?; - args.diagnostic.timing = ask_bool_if("Show timing?", args.diagnostic.timing, io)?; - args.build.clean = ask_bool_if("Clean build directory if exists?", args.build.clean, io)?; + args.connection.ssh = ask_bool_if(" Use SSH?", args.connection.ssh, io)?; + args.diagnostic.verbose = ask_bool_if(" Verbose?", args.diagnostic.verbose, io)?; + args.diagnostic.timing = ask_bool_if(" Show timing?", args.diagnostic.timing, io)?; + args.build.clean = ask_bool_if(" Clean build directory if exists?", args.build.clean, io)?; if !args.mono.mono_repo { loop { - match ask("Select mode: (1) Single Repo (2) Mono-Repo", io)?.as_str() { + match ask(" Select mode: (1) Single Repo (2) Mono-Repo", io)?.as_str() { "1" => break, "2" => { args.mono.mono_repo = true; @@ -36,14 +36,14 @@ pub fn interactive_mode(args: &mut ResolvedArgs, io: &mut IoCtx<'_>) -> Result<( if args.mono.mono_repo && args.mono.profile.is_none() && args.mono.repos.is_none() { loop { - match ask("Mono-repo: (1) Use profile (2) Manual repo list", io)?.as_str() { + match ask(" Mono-repo: (1) Use profile (2) Manual repo list", io)?.as_str() { "1" => { - args.mono.profile = Some(ask_required("Profile name", io)?); + args.mono.profile = Some(ask_required(" Profile name", io)?); break; } "2" => { let repo_list = ask_required( - "Enter repos (space separated 'username/lib1 username/lib2')", + " Enter repos (space separated 'username/lib1 username/lib2')", io, )?; args.mono.repos = Some(repo_list.split_whitespace().map(String::from).collect()); @@ -52,13 +52,14 @@ pub fn interactive_mode(args: &mut ResolvedArgs, io: &mut IoCtx<'_>) -> Result<( _ => {} } } + writeln!(io.output).ok(); } - let build_type_str = ask_default("Build type", args.build.build_type.to_cmake(), io)?; + let build_type_str = ask_default(" Build type", args.build.build_type.to_cmake(), io)?; args.build.build_type = build_type_str.parse::()?; - args.build.build_dir = ask_default("Build directory", &args.build.build_dir, io)?; - args.build.no_build = ask_bool_if("Configure only (skip build)?", args.build.no_build, io)?; + args.build.build_dir = ask_default(" Build directory", &args.build.build_dir, io)?; + args.build.no_build = ask_bool_if(" Configure only (skip build)?", args.build.no_build, io)?; - writeln!(io.output, "\nInteractive mode complete").ok(); + writeln!(io.output, " Interactive mode complete\n").ok(); Ok(()) } diff --git a/src/profile/crud.rs b/src/profile/crud.rs index cd2e21a..02a4553 100644 --- a/src/profile/crud.rs +++ b/src/profile/crud.rs @@ -41,7 +41,7 @@ pub fn add_profile( if has_profile(config, &name) && !confirm_abort( - &format!("Warning: Profile '{name}' already exists. Overwrite?"), + &format!(" Warning: Profile '{name}' already exists. Overwrite?"), yes, io, )? @@ -50,17 +50,17 @@ pub fn add_profile( } if flags.dry_run { - writeln!(io.output, "Would save profile '{name}' to config file").ok(); + 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(); + let path = save_config(config, flags.timing, &mut io.output)?; + 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, - "\nUsage: star-setup username/test-repo --profile {name}" + " Usage: star-setup username/test-repo --profile {name}" ) .ok(); Ok(()) @@ -78,7 +78,7 @@ pub fn remove_profile( ) -> Result<(), String> { let repos = match config.profiles.get(name) { None => { - writeln!(io.output, "Warning: Profile '{name}' not found.").ok(); + writeln!(io.output, " Warning: Profile '{name}' not found.").ok(); return Ok(()); } Some(r) => r.clone(), @@ -92,7 +92,7 @@ pub fn remove_profile( ); if !confirm_abort( - &format!("Are you sure you want to remove profile '{name}'?"), + &format!(" Are you sure you want to remove profile '{name}'?"), yes, io, )? { @@ -100,12 +100,16 @@ pub fn remove_profile( } if flags.dry_run { - writeln!(io.output, "Would remove profile '{name}' from config file").ok(); + 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(); + let path = save_config(config, flags.timing, &mut io.output)?; + writeln!(io.output, " Profile '{name}' removed successfully").ok(); + writeln!(io.output, " Configuration saved to: {}", path.display()).ok(); } Ok(()) } diff --git a/src/prompts.rs b/src/prompts.rs index 524ce13..dd2f7f1 100644 --- a/src/prompts.rs +++ b/src/prompts.rs @@ -56,7 +56,7 @@ pub fn ask_choice(prompt: &str, options: &[&str], io: &mut IoCtx<'_>) -> Result< writeln!(io.output, " {}) {opt}", i + 1).ok(); } loop { - let input = read_input_line("Select: ", io)?; + let input = read_input_line(" Select: ", io)?; if let Ok(n) = input.parse::() { if n >= 1 && n <= options.len() { return Ok(n - 1); @@ -104,7 +104,7 @@ pub fn confirm(prompt: &str, yes: bool, io: &mut IoCtx<'_>) -> Result) -> Result { if !confirm(warning_msg, yes, io)? { - writeln!(io.output, "Aborted.").ok(); + writeln!(io.output, " Aborted.").ok(); return Ok(false); } Ok(true) diff --git a/src/repository.rs b/src/repository.rs index a509723..7aacb93 100644 --- a/src/repository.rs +++ b/src/repository.rs @@ -1,6 +1,6 @@ //! Repository functions including cloning and URL resolution. -use crate::ctx::RunCtx; +use crate::{ctx::RunCtx, prompts::confirm}; use std::path::Path; /// Converts a repository path or URL to a local directory name (`owner-repo`). @@ -41,28 +41,37 @@ pub fn clone_repository( repo_path: &str, target_dir: &Path, use_ssh: bool, + on_exists_skip: bool, + yes: bool, ctx: &mut RunCtx<'_, '_>, ) -> Result<(), String> { let repo_name = repo_dir_name(repo_path); let repo_dir = target_dir.join(&repo_name); if repo_dir.exists() { - writeln!(ctx.io.output, "\n {repo_name} already exists").ok(); - return Ok(()); + writeln!(ctx.io.output, " Repository already exists").ok(); + if !on_exists_skip && confirm(" Update existing repository?", yes, &mut ctx.io)? { + crate::time!(ctx.flags.timing, ctx.io.output, "Update", { + pull_repository(&repo_dir, ctx)?; + }); + } + } else { + let repo_url = resolve_repo_url(repo_path, use_ssh); + crate::time!(ctx.flags.timing, ctx.io.output, "Clone", { + ctx.runner.run( + &["git", "clone", &repo_url, &repo_name], + Some(target_dir), + ctx.flags, + ctx.io.output, + )?; + writeln!(ctx.io.output, " Finished cloning {repo_name}").ok(); + Ok::<(), String>(()) + }) + .map_err(|e| format!("Failed to clone {repo_path}: {e}"))?; } - writeln!(ctx.io.output, "\n Cloning {repo_name}").ok(); - let repo_url = resolve_repo_url(repo_path, use_ssh); - - ctx - .runner - .run( - &["git", "clone", &repo_url, &repo_name], - Some(target_dir), - ctx.flags, - ctx.io.output, - ) - .map_err(|e| format!("Failed to clone {repo_path}: {e}")) + writeln!(ctx.io.output).ok(); + Ok(()) } /// Pulls the latest changes for an existing repository. diff --git a/src/run.rs b/src/run.rs index 05bd691..0f2640d 100644 --- a/src/run.rs +++ b/src/run.rs @@ -23,10 +23,15 @@ pub fn run(config_path: PathBuf) -> Result<(), Box> { let mut stdout = io::stdout(); let is_terminal = stdin.is_terminal() && stdout.is_terminal(); - let mut config = load_config(&config_locations(config_path.as_path()), &mut stdout); let mut raw = Args::parse(); - let command = raw.command.take(); let yes = raw.yes; + let command = raw.command.take(); + let mut config = load_config( + &config_locations(config_path.as_path()), + raw.diagnostic.verbose, + raw.diagnostic.timing, + &mut stdout, + ); let mut args = resolve_with_config(raw, &config).map_err(Box::::from)?; let mut flags = args.diagnostic; diff --git a/src/utils/prerequisites.rs b/src/utils/prerequisites.rs index 911cd2e..70aaf24 100644 --- a/src/utils/prerequisites.rs +++ b/src/utils/prerequisites.rs @@ -6,6 +6,10 @@ use std::process::Command; /// # Errors /// Returns an error if any required tool is missing from PATH. pub fn check_prerequisites(io: &mut IoCtx<'_>, flags: RunFlags) -> Result<(), String> { + if flags.verbose { + writeln!(io.output, "Checking Prerequisites").ok(); + } + crate::time!(flags.timing, io.output, "Check prerequisites", { let missing: Vec<&str> = ["git", "cmake", "meson"] .into_iter() @@ -16,7 +20,7 @@ pub fn check_prerequisites(io: &mut IoCtx<'_>, flags: RunFlags) -> Result<(), St .map_or(true, |o| !o.status.success()); if !is_missing && flags.verbose { - let _ = writeln!(io.output, " Found {tool}"); + writeln!(io.output, " Found {tool}").ok(); } is_missing }) @@ -26,6 +30,13 @@ pub fn check_prerequisites(io: &mut IoCtx<'_>, flags: RunFlags) -> Result<(), St return Err(format!("Missing required tools: {}", missing.join(", "))); } - Ok(()) - }) + if flags.verbose { + writeln!(io.output, " Prerequisites complete").ok(); + } + }); + + if flags.verbose || flags.timing { + writeln!(io.output).ok(); + } + Ok(()) } diff --git a/src/utils/process.rs b/src/utils/process.rs index 181cb05..dc671a7 100644 --- a/src/utils/process.rs +++ b/src/utils/process.rs @@ -71,9 +71,9 @@ pub fn run_command( }; if verbose { - writeln!(output, "Running: {}", cmd.join(" ")).ok(); + writeln!(output, " Running: {}", cmd.join(" ")).ok(); if let Some(dir) = cwd { - writeln!(output, " in directory: {}", dir.display()).ok(); + writeln!(output, " in directory: {}", dir.display()).ok(); } } diff --git a/src/utils/timing.rs b/src/utils/timing.rs index fe1b32b..962ed03 100644 --- a/src/utils/timing.rs +++ b/src/utils/timing.rs @@ -4,10 +4,11 @@ macro_rules! time { let t = std::time::Instant::now(); let result = $block; if $timing { - let _ = std::io::Write::write_fmt( + std::io::Write::write_fmt( $output, format_args!(" [timing] {}: {:.2?}\n", $msg, t.elapsed()), - ); + ) + .ok(); } result }}; diff --git a/src/workspace/clean.rs b/src/workspace/clean.rs index 1874e5d..363c3df 100644 --- a/src/workspace/clean.rs +++ b/src/workspace/clean.rs @@ -6,12 +6,14 @@ impl Workspace { /// # Errors /// Returns an error if the build directory cannot be removed. pub fn clean(&self, ctx: &mut RunCtx<'_, '_>) -> Result<(), String> { + writeln!(ctx.io.output, "Cleaning workspace").ok(); + if self.root.join("package.json").exists() { let node_modules = self.root.join("node_modules"); if !node_modules.exists() { writeln!( ctx.io.output, - "node_modules does not exist: {}", + " node_modules does not exist: {}", node_modules.display() ) .ok(); @@ -20,20 +22,22 @@ impl Workspace { if ctx.flags.dry_run { writeln!( ctx.io.output, - "Would remove directory: {}", + " Would remove directory: {}", node_modules.display() ) .ok(); } else { writeln!( ctx.io.output, - "Removing node_modules: {}", + " Removing node_modules: {}", node_modules.display() ) .ok(); - fs::remove_dir_all(&node_modules) - .map_err(|e| format!("Failed to remove node_modules: {e}"))?; - writeln!(ctx.io.output, "Done").ok(); + crate::time!(ctx.flags.timing, ctx.io.output, "Clean", { + fs::remove_dir_all(&node_modules) + .map_err(|e| format!("Failed to remove node_modules: {e}")) + })?; + writeln!(ctx.io.output, " Done").ok(); } return Ok(()); } @@ -41,7 +45,7 @@ impl Workspace { if !self.build_path.exists() { writeln!( ctx.io.output, - "Build directory does not exist: {}", + " Build directory does not exist: {}", self.build_path.display() ) .ok(); @@ -50,7 +54,7 @@ impl Workspace { writeln!( ctx.io.output, - "Removing build directory: {}", + " Removing build directory: {}", self.build_path.display() ) .ok(); @@ -58,14 +62,16 @@ impl Workspace { if ctx.flags.dry_run { writeln!( ctx.io.output, - "Would remove directory: {}", + " Would remove directory: {}", self.build_path.display() ) .ok(); } else { - fs::remove_dir_all(&self.build_path) - .map_err(|e| format!("Failed to remove build directory: {e}"))?; - writeln!(ctx.io.output, "Done").ok(); + crate::time!(ctx.flags.timing, ctx.io.output, "Clean", { + fs::remove_dir_all(&self.build_path) + .map_err(|e| format!("Failed to remove build directory: {e}")) + })?; + writeln!(ctx.io.output, " Done").ok(); } Ok(()) diff --git a/src/workspace/resolve.rs b/src/workspace/resolve.rs index 44821d4..b2fe8ca 100644 --- a/src/workspace/resolve.rs +++ b/src/workspace/resolve.rs @@ -1,4 +1,4 @@ -use crate::workspace::Workspace; +use crate::{ctx::IoCtx, workspace::Workspace}; use std::{fs, path::Path}; /// Resolves a workspace from optional path overrides. @@ -8,6 +8,8 @@ pub fn resolve_workspace( path: Option<&Path>, mono_dir: Option<&str>, build_dir: Option<&str>, + io: &mut IoCtx<'_>, + verbose: bool, ) -> Result { let base = path.unwrap_or_else(|| Path::new(".")); let root = base.join(mono_dir.unwrap_or("build-mono")); @@ -31,6 +33,10 @@ pub fn resolve_workspace( .filter(|p| p.is_dir() && p.join(".git").exists()) .collect(); + if verbose { + writeln!(io.output, " Resolved workspace: {}", root.display()).ok(); + } + Ok(Workspace { root, repos_path, diff --git a/src/workspace/status.rs b/src/workspace/status.rs index 2ae84b2..1fad91d 100644 --- a/src/workspace/status.rs +++ b/src/workspace/status.rs @@ -7,64 +7,71 @@ impl Workspace { pub fn status(&self, fetch: bool, ctx: &mut RunCtx<'_, '_>) -> Result<(), String> { writeln!(ctx.io.output, "Workspace status:\n").ok(); - for repo_dir in &self.repo_dirs { - let name = repo_dir - .file_name() - .map(|n| n.to_string_lossy().into_owned()) - .unwrap_or_default(); + crate::time!(ctx.flags.timing, ctx.io.output, "Status", { + for repo_dir in &self.repo_dirs { + let name = repo_dir + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_default(); - if ctx.flags.dry_run { - writeln!(ctx.io.output, "Would show status for {name}").ok(); - continue; - } + if ctx.flags.dry_run { + writeln!(ctx.io.output, " Would show status for {name}").ok(); + continue; + } - if fetch { - ctx - .runner - .run(&["git", "fetch"], Some(repo_dir), ctx.flags, ctx.io.output)?; - } + crate::time!(ctx.flags.timing, ctx.io.output, &name, { + if fetch { + ctx + .runner + .run(&["git", "fetch"], Some(repo_dir), ctx.flags, ctx.io.output)?; + } - let branch = ctx - .runner - .run_capture( - &["git", "rev-parse", "--abbrev-ref", "HEAD"], - Some(repo_dir), - ) - .unwrap_or_else(|_| "(unknown)".to_string()); + let branch = ctx + .runner + .run_capture( + &["git", "rev-parse", "--abbrev-ref", "HEAD"], + Some(repo_dir), + ) + .unwrap_or_else(|_| "(unknown)".to_string()); - let dirty = !ctx - .runner - .run_capture(&["git", "status", "--porcelain"], Some(repo_dir))? - .is_empty(); + let dirty = !ctx + .runner + .run_capture(&["git", "status", "--porcelain"], Some(repo_dir))? + .is_empty(); - let status_str = if dirty { "dirty" } else { "clean" }; + let status_str = if dirty { "dirty" } else { "clean" }; - let ahead_behind = if fetch { - let ahead = ctx - .runner - .run_capture( - &["git", "rev-list", "--count", "@{u}..HEAD"], - Some(repo_dir), - ) - .unwrap_or_else(|_| "?".to_string()); - let behind = ctx - .runner - .run_capture( - &["git", "rev-list", "--count", "HEAD..@{u}"], - Some(repo_dir), + let ahead_behind = if fetch { + let ahead = ctx + .runner + .run_capture( + &["git", "rev-list", "--count", "@{u}..HEAD"], + Some(repo_dir), + ) + .unwrap_or_else(|_| "?".to_string()); + let behind = ctx + .runner + .run_capture( + &["git", "rev-list", "--count", "HEAD..@{u}"], + Some(repo_dir), + ) + .unwrap_or_else(|_| "?".to_string()); + format!(" ↑{ahead} ↓{behind}") + } else { + String::new() + }; + + writeln!( + ctx.io.output, + " {name:<20} {branch:<12} {status_str}{ahead_behind}" ) - .unwrap_or_else(|_| "?".to_string()); - format!(" ↑{ahead} ↓{behind}") - } else { - String::new() - }; + .ok(); - writeln!( - ctx.io.output, - " {name:<20} {branch:<12} {status_str}{ahead_behind}" - ) - .ok(); - } + Ok::<(), String>(()) + })?; + } + Ok::<(), String>(()) + })?; Ok(()) } diff --git a/src/workspace/update.rs b/src/workspace/update.rs index ad420b0..8c3381a 100644 --- a/src/workspace/update.rs +++ b/src/workspace/update.rs @@ -14,26 +14,31 @@ impl Workspace { let mut errors: Vec = Vec::new(); - for repo_dir in &self.repo_dirs { - let name = repo_dir - .file_name() - .map(|n| n.to_string_lossy()) - .unwrap_or_default(); + crate::time!(ctx.flags.timing, ctx.io.output, "Update", { + for repo_dir in &self.repo_dirs { + let name = repo_dir + .file_name() + .map(|n| n.to_string_lossy()) + .unwrap_or_default(); - if ctx.flags.dry_run { - writeln!(ctx.io.output, "Would update {name}").ok(); - continue; - } + if ctx.flags.dry_run { + writeln!(ctx.io.output, " Would update {name}").ok(); + continue; + } - writeln!(ctx.io.output, " Updating {name}").ok(); - if let Err(e) = pull_repository(repo_dir, ctx) { - writeln!(ctx.io.output, " Failed to update {name}: {e}").ok(); - errors.push(format!("{name}: {e}")); + writeln!(ctx.io.output, " Updating {name}").ok(); + crate::time!(ctx.flags.timing, ctx.io.output, &name, { + if let Err(e) = pull_repository(repo_dir, ctx) { + writeln!(ctx.io.output, " Failed to update {name}: {e}").ok(); + errors.push(format!("{name}: {e}")); + } + }); } - } + Ok::<(), String>(()) + })?; if errors.is_empty() { - writeln!(ctx.io.output, "\nDone").ok(); + writeln!(ctx.io.output, " Done").ok(); Ok(()) } else { Err(format!( diff --git a/tests/cli/build/detect.rs b/tests/cli/build/detect.rs index 840d492..cba3642 100644 --- a/tests/cli/build/detect.rs +++ b/tests/cli/build/detect.rs @@ -78,7 +78,7 @@ fn test_detect_build_system_timing_output() { }); assert!(String::from_utf8(output) .unwrap() - .contains("[timing] Detect:")); + .contains("[timing] Scanned directory:")); } #[test] @@ -143,7 +143,7 @@ fn test_detect_mono_build_system_timing_output() { }); assert!(String::from_utf8(output) .unwrap() - .contains("[timing] Detect:")); + .contains("[timing] Scanned directories:")); } #[test] diff --git a/tests/commands/header.rs b/tests/commands/header.rs index 8250845..a27f962 100644 --- a/tests/commands/header.rs +++ b/tests/commands/header.rs @@ -13,6 +13,7 @@ fn test_print_mode_header_repo_name_without_test_repo() { mono_dir: None, profile: None, lib_count: None, + repo_count: None, }, io, ); diff --git a/tests/commands/mono/resolve.rs b/tests/commands/mono/resolve.rs index fbade92..4e7604b 100644 --- a/tests/commands/mono/resolve.rs +++ b/tests/commands/mono/resolve.rs @@ -53,7 +53,7 @@ fn test_resolve_repos_for_mono_empty_profile_errors() { args.mono.profile = Some("emptyprofile".to_string()); with_io(|io| { - let result = resolve_repos_for_mono(&args, &config, "user/repo", io); + let result = resolve_repos_for_mono(&args, &config, io); assert!(result.is_err()); assert!(result.unwrap_err().contains("has no repositories")); }); @@ -70,7 +70,7 @@ fn test_resolve_repos_for_mono_with_profile() { args.mono.profile = Some("myprofile".to_string()); with_io(|io| { - let result = resolve_repos_for_mono(&args, &config, "user/repo", io); + let result = resolve_repos_for_mono(&args, &config, io); assert!(result.is_ok()); assert_eq!(result.unwrap(), vec!["user/lib1", "user/lib2"]); }); @@ -83,7 +83,7 @@ fn test_resolve_repos_for_mono_with_explicit_repos() { args.mono.repos = Some(vec!["user/lib1".to_string(), "user/lib2".to_string()]); with_io(|io| { - let result = resolve_repos_for_mono(&args, &config, "user/repo", io); + let result = resolve_repos_for_mono(&args, &config, io); assert!(result.is_ok()); assert_eq!(result.unwrap(), vec!["user/lib1", "user/lib2"]); }); @@ -95,7 +95,7 @@ fn test_resolve_repos_for_mono_no_repos_or_profile_errors() { let args = default_resolved(); with_io(|io| { - let result = resolve_repos_for_mono(&args, &config, "user/repo", io); + let result = resolve_repos_for_mono(&args, &config, io); assert!(result.is_err()); assert!(result .unwrap_err() @@ -110,7 +110,7 @@ fn test_resolve_repos_for_mono_profile_not_found_errors() { args.mono.profile = Some("nonexistent".to_string()); with_io(|io| { - let result = resolve_repos_for_mono(&args, &config, "user/repo", io); + let result = resolve_repos_for_mono(&args, &config, io); assert!(result.is_err()); assert!(result.unwrap_err().contains("not found")); }); diff --git a/tests/config/crud.rs b/tests/config/crud.rs index 207f688..b06d068 100644 --- a/tests/config/crud.rs +++ b/tests/config/crud.rs @@ -145,7 +145,7 @@ fn test_remove_config_removes_and_saves() { let mut config = SetupConfig::new(); config.path = Some(path.clone()); insert_config(&mut config, "myconfig", sample_entry()); - save_config(&mut config).unwrap(); + save_config(&mut config, false, &mut io.output).unwrap(); remove_config(&mut config, "myconfig", true, io, make_flags()).unwrap(); assert!(!has_config(&config, "myconfig")); diff --git a/tests/config/io.rs b/tests/config/io.rs index 0baaccb..2a706ef 100644 --- a/tests/config/io.rs +++ b/tests/config/io.rs @@ -30,10 +30,10 @@ fn test_save_and_load_roundtrip() { meson_flags: vec![], }, ); - save_config(&mut config).unwrap(); with_io_output(|io| { - let loaded = load_config(&[path], &mut io.output); + save_config(&mut config, false, &mut io.output).unwrap(); + let loaded = load_config(&[path], false, false, &mut io.output); assert!(loaded.configs.contains_key("default")); assert!(loaded.configs["default"].ssh); assert_eq!(loaded.configs["default"].build_type, BuildType::Release); @@ -45,7 +45,7 @@ fn test_save_and_load_roundtrip() { #[test] fn test_load_config_skips_missing_local_file() { with_io_output(|io| { - let config = load_config(&[], &mut io.output); + let config = load_config(&[], false, false, &mut io.output); assert!(config.configs.is_empty()); }); } @@ -57,7 +57,7 @@ fn test_load_config_handles_invalid_json() { std::fs::write(&path, "{invalid json").unwrap(); with_io_output(|io| { - let config = load_config(&[path], &mut io.output); + let config = load_config(&[path], false, false, &mut io.output); assert!(config.configs.is_empty()); }); } @@ -67,6 +67,8 @@ fn test_load_config_skips_nonexistent_path() { with_io_output(|io| { let config = load_config( &[PathBuf::from("/nonexistent/path/.star-setup.json")], + false, + false, &mut io.output, ); assert!(config.configs.is_empty()); @@ -81,17 +83,18 @@ fn test_load_config_first_valid_wins() { let path2 = tmp2.path().join(".star-setup.json"); let mut config1 = SetupConfig::new(); - config1.path = Some(path1.clone()); - insert_config(&mut config1, "first", sample_entry()); - save_config(&mut config1).unwrap(); - let mut config2 = SetupConfig::new(); - config2.path = Some(path2.clone()); - insert_config(&mut config2, "second", sample_entry()); - save_config(&mut config2).unwrap(); with_io_output(|io| { - let loaded = load_config(&[path1, path2], &mut io.output); + config1.path = Some(path1.clone()); + insert_config(&mut config1, "first", sample_entry()); + save_config(&mut config1, false, &mut io.output).unwrap(); + + config2.path = Some(path2.clone()); + insert_config(&mut config2, "second", sample_entry()); + save_config(&mut config2, false, &mut io.output).unwrap(); + + let loaded = load_config(&[path1, path2], false, false, &mut io.output); assert!(loaded.configs.contains_key("first")); assert!(!loaded.configs.contains_key("second")); }); @@ -109,10 +112,10 @@ fn test_load_config_falls_through_invalid_to_valid() { let mut config2 = SetupConfig::new(); config2.path = Some(path2.clone()); insert_config(&mut config2, "second", sample_entry()); - save_config(&mut config2).unwrap(); with_io_output(|io| { - let loaded = load_config(&[path1, path2], &mut io.output); + save_config(&mut config2, false, &mut io.output).unwrap(); + let loaded = load_config(&[path1, path2], false, false, &mut io.output); assert!(loaded.configs.contains_key("second")); }); } diff --git a/tests/ctx.rs b/tests/ctx.rs index ebb3ac4..93395b0 100644 --- a/tests/ctx.rs +++ b/tests/ctx.rs @@ -21,7 +21,7 @@ fn test_dry_run_runner_prints_command() { .run(&["git", "clone", "foo"], None, make_flags(), io.output) .unwrap(); }); - assert_eq!(output, "Would run: git clone foo\n"); + assert_eq!(output, " Would run: git clone foo\n"); } #[test] @@ -36,7 +36,7 @@ fn test_dry_run_runner_prints_cwd() { ) .unwrap(); }); - assert!(output.contains("Would run: cmake ..")); + assert!(output.contains(" Would run: cmake ..")); assert!(output.contains(" in directory: /tmp/build")); } diff --git a/tests/profile/crud.rs b/tests/profile/crud.rs index 532c2a7..de97b93 100644 --- a/tests/profile/crud.rs +++ b/tests/profile/crud.rs @@ -122,7 +122,7 @@ fn test_remove_profile_removes_and_saves() { let mut config = SetupConfig::new(); config.path = Some(path.clone()); insert_profile(&mut config, "myprofile", vec!["user/repo1".to_string()]); - save_config(&mut config).unwrap(); + save_config(&mut config, false, &mut io.output).unwrap(); remove_profile(&mut config, "myprofile", true, io, make_flags()).unwrap(); assert!(!has_profile(&config, "myprofile")); @@ -159,10 +159,11 @@ fn test_save_and_load_profile_roundtrip() { "myprofile", vec!["user/repo1".to_string(), "user/repo2".to_string()], ); - save_config(&mut config).unwrap(); with_io_output(|io| { - let loaded = load_config(&[path], &mut io.output); + save_config(&mut config, false, &mut io.output).unwrap(); + + let loaded = load_config(&[path], false, false, &mut io.output); assert!(loaded.profiles.contains_key("myprofile")); assert_eq!( loaded.profiles["myprofile"], diff --git a/tests/repository.rs b/tests/repository.rs index db32eaa..7775c77 100644 --- a/tests/repository.rs +++ b/tests/repository.rs @@ -70,7 +70,7 @@ fn test_clone_skips_existing_directory() { let repo_dir = tmp_path.join("owner-repo"); std::fs::create_dir_all(&repo_dir).unwrap(); - let result = clone_repository("owner/repo", tmp_path, false, ctx); + let result = clone_repository("owner/repo", tmp_path, false, true, false, ctx); assert!(result.is_ok()); assert!(repo_dir.exists()); }); @@ -81,7 +81,7 @@ fn test_clone_repository_calls_git_clone() { let tmp = tempfile::TempDir::new().unwrap(); let runner = with_ctx_runner(MockRunner::new(), |_, ctx| { - clone_repository("user/repo", tmp.path(), false, ctx).unwrap(); + clone_repository("user/repo", tmp.path(), false, true, false, ctx).unwrap(); }); assert_eq!(runner.calls.len(), 1); diff --git a/tests/workspace/resolve.rs b/tests/workspace/resolve.rs index e234ee5..657ea26 100644 --- a/tests/workspace/resolve.rs +++ b/tests/workspace/resolve.rs @@ -5,8 +5,8 @@ use crate::common::with_io_dir; #[test] fn test_resolve_workspace_errors_when_missing() { - with_io_dir(|path, _| { - let result = resolve_workspace(Some(path), None, None); + with_io_dir(|path, io| { + let result = resolve_workspace(Some(path), None, None, io, false); assert!(result.is_err()); assert!(result.unwrap_err().contains("Workspace not found")); }); @@ -14,9 +14,9 @@ fn test_resolve_workspace_errors_when_missing() { #[test] fn test_resolve_workspace_errors_when_no_repos() { - with_io_dir(|path, _| { + with_io_dir(|path, io| { fs::create_dir_all(path.join("build-mono")).unwrap(); - let result = resolve_workspace(Some(path), None, None); + let result = resolve_workspace(Some(path), None, None, io, false); assert!(result.is_err()); assert!(result.unwrap_err().contains("Repos directory not found")); }); @@ -24,9 +24,9 @@ fn test_resolve_workspace_errors_when_no_repos() { #[test] fn test_resolve_workspace_succeeds() { - with_io_dir(|path, _| { + with_io_dir(|path, io| { fs::create_dir_all(path.join("build-mono").join("repos")).unwrap(); - let result = resolve_workspace(Some(path), None, None); + let result = resolve_workspace(Some(path), None, None, io, false); assert!(result.is_ok()); let ws = result.unwrap(); assert_eq!(ws.repo_dirs.len(), 0); @@ -35,40 +35,40 @@ fn test_resolve_workspace_succeeds() { #[test] fn test_resolve_workspace_finds_repos() { - with_io_dir(|path, _| { + with_io_dir(|path, io| { let repos = path.join("build-mono").join("repos"); fs::create_dir_all(repos.join("user-lib1").join(".git")).unwrap(); fs::create_dir_all(repos.join("user-lib2").join(".git")).unwrap(); - let ws = resolve_workspace(Some(path), None, None).unwrap(); + let ws = resolve_workspace(Some(path), None, None, io, false).unwrap(); assert_eq!(ws.repo_dirs.len(), 2); }); } #[test] fn test_resolve_workspace_custom_mono_dir() { - with_io_dir(|path, _| { + with_io_dir(|path, io| { fs::create_dir_all(path.join("my-workspace").join("repos")).unwrap(); - let result = resolve_workspace(Some(path), Some("my-workspace"), None); + let result = resolve_workspace(Some(path), Some("my-workspace"), None, io, false); assert!(result.is_ok()); }); } #[test] fn test_resolve_workspace_custom_build_dir() { - with_io_dir(|path, _| { + with_io_dir(|path, io| { fs::create_dir_all(path.join("build-mono").join("repos")).unwrap(); - let ws = resolve_workspace(Some(path), None, Some("out")).unwrap(); + let ws = resolve_workspace(Some(path), None, Some("out"), io, false).unwrap(); assert!(ws.build_path.ends_with("out")); }); } #[test] fn test_resolve_workspace_excludes_non_git_dirs() { - with_io_dir(|path, _| { + with_io_dir(|path, io| { let repos = path.join("build-mono").join("repos"); fs::create_dir_all(repos.join("user-lib1").join(".git")).unwrap(); fs::create_dir_all(repos.join("not-a-repo")).unwrap(); - let ws = resolve_workspace(Some(path), None, None).unwrap(); + let ws = resolve_workspace(Some(path), None, None, io, false).unwrap(); assert_eq!(ws.repo_dirs.len(), 1); }); } From 66e5f320f4e1112ca09fc573122acb20665117b1 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 2 Jul 2026 21:55:38 -0400 Subject: [PATCH 12/22] refactor: extract dry_run_or_do helper for cleanup/creation --- src/commands/mono/mode.rs | 23 +++++++--------- src/commands/setup.rs | 54 ++++++++++++++++++------------------- src/utils/dry_run.rs | 20 ++++++++++++++ src/utils/mod.rs | 2 ++ src/workspace/clean.rs | 57 +++++++++++++-------------------------- 5 files changed, 76 insertions(+), 80 deletions(-) create mode 100644 src/utils/dry_run.rs diff --git a/src/commands/mono/mode.rs b/src/commands/mono/mode.rs index e04716d..3238db3 100644 --- a/src/commands/mono/mode.rs +++ b/src/commands/mono/mode.rs @@ -12,6 +12,7 @@ use crate::{ config::SetupConfig, ctx::RunCtx, repository::repo_dir_name, + utils::dry_run_or_do, }; use std::{ fs, @@ -51,22 +52,16 @@ pub fn mono_repo_mode( &mut ctx.io, ); - writeln!(ctx.io.output, "Creating repo folder").ok(); let mono_repo_path = base_dir.join(&args.mono.mono_dir); let repos_path = mono_repo_path.join("repos"); - if ctx.flags.dry_run { - writeln!( - ctx.io.output, - " Would create directory: {}", - repos_path.display() - ) - .ok(); - } else { - crate::time!(ctx.flags.timing, ctx.io.output, "Create directory", { - fs::create_dir_all(&repos_path).map_err(|e| e.to_string())?; - }); - } - writeln!(ctx.io.output, " Finished creating\n").ok(); + dry_run_or_do( + "create directory", + "Creating", + &repos_path, + ctx, + "Create directory", + || fs::create_dir_all(&repos_path).map_err(|e| e.to_string()), + )?; clone_mono_repos(&repos, &repos_path, args.connection.ssh, ctx)?; diff --git a/src/commands/setup.rs b/src/commands/setup.rs index 293ee7c..737d110 100644 --- a/src/commands/setup.rs +++ b/src/commands/setup.rs @@ -2,6 +2,7 @@ use crate::{ cli::{BuildSystem, ResolvedArgs}, commands::build_project, ctx::RunCtx, + utils::dry_run_or_do, }; use std::{fs, path::Path}; @@ -15,37 +16,34 @@ pub fn prepare_build_dir( ) -> Result<(), String> { if clean { writeln!(ctx.io.output, "Cleaning build directory").ok(); - if ctx.flags.dry_run { - writeln!( - ctx.io.output, - " Would remove directory: {}", - build_path.display() - ) - .ok(); - } else if build_path.exists() { - crate::time!(ctx.flags.timing, ctx.io.output, "Clean", { - fs::remove_dir_all(build_path).map_err(|e| e.to_string())?; - }); - } - writeln!(ctx.io.output, " Finished cleaning").ok(); - writeln!(ctx.io.output).ok(); + dry_run_or_do( + "remove directory", + "Removing", + build_path, + ctx, + "Clean", + || { + if build_path.exists() { + fs::remove_dir_all(build_path).map_err(|e| e.to_string()) + } else { + Ok(()) + } + }, + )?; + writeln!(ctx.io.output, " Finished cleaning\n").ok(); } writeln!(ctx.io.output, "Creating build directory").ok(); - if ctx.flags.dry_run { - writeln!( - ctx.io.output, - " Would create directory: {}", - build_path.display() - ) - .ok(); - } else { - crate::time!(ctx.flags.timing, ctx.io.output, "Create build directory", { - fs::create_dir_all(build_path).map_err(|e| e.to_string())?; - }); - } - writeln!(ctx.io.output, " Finished creating").ok(); - writeln!(ctx.io.output).ok(); + dry_run_or_do( + "create directory", + "Creating", + build_path, + ctx, + "Create build directory", + || fs::create_dir_all(build_path).map_err(|e| e.to_string()), + )?; + + writeln!(ctx.io.output, " Finished creating\n").ok(); Ok(()) } diff --git a/src/utils/dry_run.rs b/src/utils/dry_run.rs new file mode 100644 index 0000000..22a8c6b --- /dev/null +++ b/src/utils/dry_run.rs @@ -0,0 +1,20 @@ +use crate::ctx::RunCtx; +use std::path::Path; + +pub fn dry_run_or_do( + verb: &str, + progressive: &str, + path: &Path, + ctx: &mut RunCtx, + timer_label: &str, + op: impl FnOnce() -> Result<(), String>, +) -> Result<(), String> { + if ctx.flags.dry_run { + writeln!(ctx.io.output, " Would {verb}: {}", path.display()).ok(); + } else { + writeln!(ctx.io.output, " {progressive}: {}", path.display()).ok(); + crate::time!(ctx.flags.timing, ctx.io.output, timer_label, { op() })?; + writeln!(ctx.io.output, " Done").ok(); + } + Ok(()) +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index e6a147d..47c2f12 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -2,4 +2,6 @@ pub mod prerequisites; pub use prerequisites::check_prerequisites; pub mod process; pub use process::run_command; +pub mod dry_run; pub mod timing; +pub use dry_run::dry_run_or_do; diff --git a/src/workspace/clean.rs b/src/workspace/clean.rs index 363c3df..c999c2b 100644 --- a/src/workspace/clean.rs +++ b/src/workspace/clean.rs @@ -1,4 +1,4 @@ -use crate::{ctx::RunCtx, workspace::Workspace}; +use crate::{ctx::RunCtx, utils::dry_run_or_do, workspace::Workspace}; use std::fs; impl Workspace { @@ -19,26 +19,17 @@ impl Workspace { .ok(); return Ok(()); } - if ctx.flags.dry_run { - writeln!( - ctx.io.output, - " Would remove directory: {}", - node_modules.display() - ) - .ok(); - } else { - writeln!( - ctx.io.output, - " Removing node_modules: {}", - node_modules.display() - ) - .ok(); - crate::time!(ctx.flags.timing, ctx.io.output, "Clean", { + dry_run_or_do( + "remove directory", + "Removing", + &node_modules, + ctx, + "Clean", + || { fs::remove_dir_all(&node_modules) .map_err(|e| format!("Failed to remove node_modules: {e}")) - })?; - writeln!(ctx.io.output, " Done").ok(); - } + }, + )?; return Ok(()); } @@ -52,27 +43,17 @@ impl Workspace { return Ok(()); } - writeln!( - ctx.io.output, - " Removing build directory: {}", - self.build_path.display() - ) - .ok(); - - if ctx.flags.dry_run { - writeln!( - ctx.io.output, - " Would remove directory: {}", - self.build_path.display() - ) - .ok(); - } else { - crate::time!(ctx.flags.timing, ctx.io.output, "Clean", { + dry_run_or_do( + "remove directory", + "Removing", + &self.build_path, + ctx, + "Clean", + || { fs::remove_dir_all(&self.build_path) .map_err(|e| format!("Failed to remove build directory: {e}")) - })?; - writeln!(ctx.io.output, " Done").ok(); - } + }, + )?; Ok(()) } From a080656c5f196ad750a1d27bd585b2368006733a Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 2 Jul 2026 22:00:23 -0400 Subject: [PATCH 13/22] refactor: extract detect_or_dry_run helper for build system detection --- src/commands/mono/mode.rs | 22 ++++------------------ src/commands/single.rs | 21 ++++----------------- src/utils/dry_run.rs | 37 ++++++++++++++++++++++++++++++++++++- 3 files changed, 44 insertions(+), 36 deletions(-) diff --git a/src/commands/mono/mode.rs b/src/commands/mono/mode.rs index 3238db3..afd2cf6 100644 --- a/src/commands/mono/mode.rs +++ b/src/commands/mono/mode.rs @@ -12,7 +12,7 @@ use crate::{ config::SetupConfig, ctx::RunCtx, repository::repo_dir_name, - utils::dry_run_or_do, + utils::{dry_run::detect_or_dry_run, dry_run_or_do}, }; use std::{ fs, @@ -70,24 +70,10 @@ pub fn mono_repo_mode( .map(|r| repos_path.join(repo_dir_name(r))) .collect(); - writeln!(ctx.io.output, "Detecting build system").ok(); let build_path = mono_repo_path.join(&args.build.build_dir); - let build_system = crate::time!(ctx.flags.timing, ctx.io.output, "Detect", { - let result = if let Some(bs) = args.build.build_system { - if ctx.flags.verbose { - writeln!(ctx.io.output, " Build system flag set: {bs:?}").ok(); - } - Some(bs) - } else if ctx.flags.dry_run { - writeln!(ctx.io.output, " Would detect build system after cloning").ok(); - None - } else { - Some(detect_mono_build_system(&repo_dirs, ctx)?) - }; - writeln!(ctx.io.output, " Finished detecting").ok(); - result - }); - writeln!(ctx.io.output).ok(); + let build_system = detect_or_dry_run(args.build.build_system, ctx, |ctx| { + detect_mono_build_system(&repo_dirs, ctx) + })?; let canonical_map = if let Some(bs) = build_system { let map = generate_mono_config(bs, &mono_repo_path, &repos_path, &repo_dirs, &repos, ctx)?; diff --git a/src/commands/single.rs b/src/commands/single.rs index 51f0fd9..cc1eb7e 100644 --- a/src/commands/single.rs +++ b/src/commands/single.rs @@ -5,6 +5,7 @@ use crate::{ }, ctx::RunCtx, repository::{clone_repository, repo_dir_name}, + utils::dry_run::detect_or_dry_run, }; use std::path::Path; @@ -38,24 +39,10 @@ pub fn single_repo_mode( writeln!(ctx.io.output, "Cloning repository").ok(); clone_repository(repo, base_dir, args.connection.ssh, false, args.yes, ctx)?; - writeln!(ctx.io.output, "Detecting build system").ok(); let build_path = repo_path.join(&args.build.build_dir); - let build_system = crate::time!(ctx.flags.timing, ctx.io.output, "Detect", { - let result = if let Some(bs) = args.build.build_system { - if ctx.flags.verbose { - writeln!(ctx.io.output, " Build system flag set: {bs:?}").ok(); - } - Some(bs) - } else if ctx.flags.dry_run { - writeln!(ctx.io.output, " Would detect build system after cloning").ok(); - None - } else { - Some(detect_build_system(&repo_path, ctx)?) - }; - writeln!(ctx.io.output, " Finished detecting").ok(); - result - }); - writeln!(ctx.io.output).ok(); + let build_system = detect_or_dry_run(args.build.build_system, ctx, |ctx| { + detect_build_system(&repo_path, ctx) + })?; if let Some(build_system) = build_system { if build_system == BuildSystem::Npm { diff --git a/src/utils/dry_run.rs b/src/utils/dry_run.rs index 22a8c6b..fe0ddcb 100644 --- a/src/utils/dry_run.rs +++ b/src/utils/dry_run.rs @@ -1,6 +1,10 @@ -use crate::ctx::RunCtx; +use crate::{cli::BuildSystem, ctx::RunCtx}; use std::path::Path; + +/// Prints a dry-run message or executes `op`, timing it if not a dry run. +/// # Errors +/// Returns an error if `op` fails. pub fn dry_run_or_do( verb: &str, progressive: &str, @@ -18,3 +22,34 @@ pub fn dry_run_or_do( } Ok(()) } + +/// Detects the build system or prints what would be detected in a dry run. +/// # Errors +/// Returns an error if `detect_fn` fails. +pub fn detect_or_dry_run( + bs_flag: Option, + ctx: &mut RunCtx, + detect_fn: F, +) -> Result, String> +where + F: FnOnce(&mut RunCtx) -> Result, +{ + writeln!(ctx.io.output, "Detecting build system").ok(); + let result = crate::time!(ctx.flags.timing, ctx.io.output, "Detect", { + let r = if let Some(bs) = bs_flag { + if ctx.flags.verbose { + writeln!(ctx.io.output, " Build system flag set: {bs:?}").ok(); + } + Some(bs) + } else if ctx.flags.dry_run { + writeln!(ctx.io.output, " Would detect build system after cloning").ok(); + None + } else { + Some(detect_fn(ctx)?) + }; + writeln!(ctx.io.output, " Finished detecting").ok(); + r + }); + writeln!(ctx.io.output).ok(); + Ok(result) +} From 165c8379eb95a6ec6c0eaad68d07b3bc38e456dc Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 2 Jul 2026 22:04:49 -0400 Subject: [PATCH 14/22] refactor: extract io_error_msg helper for load/save config errors --- src/config/io.rs | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/src/config/io.rs b/src/config/io.rs index 01b736f..d5c6ca2 100644 --- a/src/config/io.rs +++ b/src/config/io.rs @@ -1,5 +1,5 @@ use crate::config::SetupConfig; -use std::{fs, io, io::Write, path::PathBuf}; +use std::{fs, io::{self, Write}, path::{Path, PathBuf}}; /// Returns the list of paths to search for a config file. #[must_use] @@ -13,6 +13,13 @@ pub fn config_locations(path: &std::path::Path) -> Vec { .collect() } +fn io_error_msg(verb: &str, path: &Path, e: io::Error) -> String { + match e.kind() { + io::ErrorKind::PermissionDenied => format!("Error: No permission to {verb} {}", path.display()), + _ => format!("An unexpected error occurred: {verb} {}: {e}", path.display()), + } +} + /// Loads configuration from the first valid JSON file in `locations`. pub fn load_config( locations: &[PathBuf], @@ -45,18 +52,8 @@ pub fn load_config( None } }, - Err(e) if e.kind() == io::ErrorKind::PermissionDenied => { - writeln!(output, " Error: No permission to read {}", path.display()).ok(); - invalid_count += 1; - None - } Err(e) => { - writeln!( - output, - " An unexpected error occurred reading {}: {e}", - path.display() - ) - .ok(); + writeln!(output, " {}", io_error_msg("read", path, e)).ok(); invalid_count += 1; None } @@ -100,16 +97,7 @@ pub fn save_config( serde_json::to_string_pretty(config).map_err(|e| format!("Failed to serialize config: {e}"))?; crate::time!(timing, output, "Write config", { - fs::write(&path, json).map_err(|e| match e.kind() { - io::ErrorKind::PermissionDenied => { - format!("Error: No permission to write to {}", path.display()) - } - _ => format!( - "An unexpected error occurred writing {}: {}", - path.display(), - e - ), - })?; + fs::write(&path, json).map_err(|e| io_error_msg("write to", &path, e))?; }); Ok(path) From 029b2ecb589a9847278add11bc942901361377bd Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 2 Jul 2026 23:32:49 -0400 Subject: [PATCH 15/22] refactor: gate dry-run/verbose output in dry_run_or_do, fix build_project arg order --- src/cli/build/detect.rs | 1 - src/cli/commands.rs | 41 ++++++++++++------------- src/commands/build.rs | 6 ++-- src/commands/handlers.rs | 62 ++++++++++++-------------------------- src/commands/mod.rs | 2 +- src/commands/mono/clone.rs | 1 - src/commands/mono/mode.rs | 24 +++++++++------ src/commands/mono/watch.rs | 6 ++-- src/commands/setup.rs | 24 ++------------- src/commands/single.rs | 17 +++-------- src/config/display.rs | 2 +- src/config/io.rs | 28 +++++++++++++---- src/ctx.rs | 10 +++--- src/interactive.rs | 1 - src/profile/crud.rs | 2 +- src/profile/display.rs | 2 +- src/repository.rs | 4 ++- src/run.rs | 2 +- src/utils/dry_run.rs | 21 +++++++++---- tests/commands/single.rs | 2 +- tests/ctx.rs | 13 ++++++-- tests/workspace/clean.rs | 1 + 22 files changed, 131 insertions(+), 141 deletions(-) diff --git a/src/cli/build/detect.rs b/src/cli/build/detect.rs index 555a8ce..9493c5b 100644 --- a/src/cli/build/detect.rs +++ b/src/cli/build/detect.rs @@ -59,7 +59,6 @@ pub fn detect_mono_build_system( dirs: &[PathBuf], ctx: &mut RunCtx<'_, '_>, ) -> Result { - writeln!(ctx.io.output, " Detecting build system\n").ok(); crate::time!(ctx.flags.timing, ctx.io.output, "Scanned directories", { let mut detected = Vec::new(); if dirs.iter().all(|d| d.join("CMakeLists.txt").exists()) { diff --git a/src/cli/commands.rs b/src/cli/commands.rs index 50ca16f..0036324 100644 --- a/src/cli/commands.rs +++ b/src/cli/commands.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use crate::cli::{BuildFlags, ConnectionFlags, DiagnosticFlags, MonoRepoFlags}; -use clap::{Parser, Subcommand}; +use clap::{Args as ClapArgs, Parser, Subcommand}; /// Config subcommand. #[derive(Parser)] @@ -70,38 +70,35 @@ pub struct WorkspaceCommand { pub action: WorkspaceAction, } -/// Workspace subcommand actions. +#[derive(ClapArgs)] +pub struct WorkspaceTarget { + /// Workspace root directory (default: current directory). + #[arg(long)] + pub path: Option, + /// Mono-repo workspace directory name (default: build-mono). + #[arg(long)] + pub mono_dir: Option, + #[arg(long)] + pub build_dir: Option, +} + #[derive(Subcommand)] pub enum WorkspaceAction { /// Pull latest changes for all repos in the workspace. Update { - /// Workspace root directory (default: current directory). - #[arg(long)] - path: Option, - /// Mono-repo workspace directory name (default: build-mono). - #[arg(long)] - mono_dir: Option, - #[arg(long)] - build_dir: Option, + #[command(flatten)] + target: WorkspaceTarget, }, /// Show status of all repos in the workspace. Status { - #[arg(long)] - path: Option, - #[arg(long)] - mono_dir: Option, - #[arg(long)] - build_dir: Option, + #[command(flatten)] + target: WorkspaceTarget, #[arg(long)] fetch: bool, }, /// Remove the build directory from the workspace. Clean { - #[arg(long)] - path: Option, - #[arg(long)] - mono_dir: Option, - #[arg(long)] - build_dir: Option, + #[command(flatten)] + target: WorkspaceTarget, }, } diff --git a/src/commands/build.rs b/src/commands/build.rs index cb75317..cadb08d 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -130,9 +130,11 @@ pub fn build_project( mono: bool, ctx: &mut RunCtx<'_, '_>, ) -> Result<(), String> { - match build_system { + let result = match build_system { BuildSystem::Cmake => cmake_build(args, build_path, mono, ctx), BuildSystem::Meson => meson_build(args, build_path, source_path, ctx), BuildSystem::Npm => npm_build(args, source_path, mono, ctx), - } + }; + writeln!(ctx.io.output).ok(); + result } diff --git a/src/commands/handlers.rs b/src/commands/handlers.rs index 17f2e2a..7422c72 100644 --- a/src/commands/handlers.rs +++ b/src/commands/handlers.rs @@ -63,54 +63,32 @@ pub fn handle_profile_cmd( /// # Errors /// Returns an error if resolving, updating, cleaning, or fetching status for the workspace fails. pub fn handle_workspace_cmd( - action: WorkspaceAction, + action: &WorkspaceAction, mut io: IoCtx, flags: RunFlags, ) -> Result<(), Box> { + let target = match &action { + WorkspaceAction::Update { target } + | WorkspaceAction::Clean { target } + | WorkspaceAction::Status { target, .. } => target, + }; + let ws = resolve_workspace( + target.path.as_deref(), + target.mono_dir.as_deref(), + target.build_dir.as_deref(), + &mut io, + flags.verbose, + )?; + match action { - WorkspaceAction::Update { - path, - mono_dir, - build_dir, - } => { - let ws = resolve_workspace( - path.as_deref(), - mono_dir.as_deref(), - build_dir.as_deref(), - &mut io, - flags.verbose, - )?; - with_runner(io, flags, |ctx| ws.update(ctx).map_err(Into::into))?; + WorkspaceAction::Update { .. } => { + with_runner(io, flags, |ctx| ws.update(ctx).map_err(Into::into)) } - WorkspaceAction::Status { - path, - mono_dir, - build_dir, - fetch, - } => { - let ws = resolve_workspace( - path.as_deref(), - mono_dir.as_deref(), - build_dir.as_deref(), - &mut io, - flags.verbose, - )?; - with_runner(io, flags, |ctx| ws.status(fetch, ctx).map_err(Into::into))?; + WorkspaceAction::Status { fetch, .. } => { + with_runner(io, flags, |ctx| ws.status(*fetch, ctx).map_err(Into::into)) } - WorkspaceAction::Clean { - path, - mono_dir, - build_dir, - } => { - let ws = resolve_workspace( - path.as_deref(), - mono_dir.as_deref(), - build_dir.as_deref(), - &mut io, - flags.verbose, - )?; - with_runner(io, flags, |ctx| ws.clean(ctx).map_err(Into::into))?; + WorkspaceAction::Clean { .. } => { + with_runner(io, flags, |ctx| ws.clean(ctx).map_err(Into::into)) } } - Ok(()) } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 962e294..2e1b928 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -12,6 +12,6 @@ pub use mono::{ pub mod single; pub use single::single_repo_mode; pub mod setup; -pub use setup::{configure_and_build, extract_repo_input, prepare_build_dir}; +pub use setup::{extract_repo_input, prepare_build_dir}; pub mod handlers; pub use handlers::{handle_config_cmd, handle_profile_cmd, handle_workspace_cmd}; diff --git a/src/commands/mono/clone.rs b/src/commands/mono/clone.rs index a4cd2db..5a93d60 100644 --- a/src/commands/mono/clone.rs +++ b/src/commands/mono/clone.rs @@ -28,7 +28,6 @@ pub fn clone_mono_repos( .ok(); Ok::<(), String>(()) })?; - writeln!(ctx.io.output).ok(); Ok(()) } diff --git a/src/commands/mono/mode.rs b/src/commands/mono/mode.rs index afd2cf6..efb58d6 100644 --- a/src/commands/mono/mode.rs +++ b/src/commands/mono/mode.rs @@ -1,7 +1,7 @@ use crate::{ cli::{detect_mono_build_system, BuildSystem, ResolvedArgs}, commands::{ - build_repo_list, configure_and_build, extract_repo_input, + build_project, build_repo_list, extract_repo_input, mono::{ clone_mono_repos, display::{resolve_setup_paths, SetupPaths}, @@ -54,6 +54,9 @@ pub fn mono_repo_mode( let mono_repo_path = base_dir.join(&args.mono.mono_dir); let repos_path = mono_repo_path.join("repos"); + if ctx.flags.verbose { + writeln!(ctx.io.output, "Creating directory").ok(); + } dry_run_or_do( "create directory", "Creating", @@ -62,6 +65,9 @@ pub fn mono_repo_mode( "Create directory", || fs::create_dir_all(&repos_path).map_err(|e| e.to_string()), )?; + if ctx.flags.verbose { + writeln!(ctx.io.output).ok(); + } clone_mono_repos(&repos, &repos_path, args.connection.ssh, ctx)?; @@ -80,18 +86,18 @@ pub fn mono_repo_mode( if bs != BuildSystem::Npm { prepare_build_dir(build_path.as_path(), args.build.clean, ctx)?; } - configure_and_build(args, &mono_repo_path, &build_path, bs, true, ctx)?; + build_project(args, &build_path, &mono_repo_path, bs, true, ctx)?; map } else { - prepare_build_dir(build_path.as_path(), args.build.clean, ctx)?; None }; - if build_system == Some(BuildSystem::Npm) && !args.build.no_watch && !ctx.flags.dry_run { - generate_watch_scripts(&mono_repo_path, &repos_path, &repos, &mut ctx.io, ctx.flags)?; - if args.build.watch { - open_watch_scripts(&mono_repo_path, &mut ctx.io, ctx.flags)?; - } + if build_system == Some(BuildSystem::Npm) + && !args.build.no_watch + && generate_watch_scripts(&mono_repo_path, &repos_path, &repos, &mut ctx.io, ctx.flags)? + && args.build.watch + { + open_watch_scripts(&mono_repo_path, &mut ctx.io, ctx.flags)?; } let paths = if ctx.flags.dry_run { @@ -114,7 +120,7 @@ pub fn mono_repo_mode( ) }; - if build_system.is_none() && ctx.flags.dry_run { + if build_system.is_none() { writeln!( ctx.io.output, "Would finish setup in {}", diff --git a/src/commands/mono/watch.rs b/src/commands/mono/watch.rs index 0cadc39..25dad63 100644 --- a/src/commands/mono/watch.rs +++ b/src/commands/mono/watch.rs @@ -64,11 +64,11 @@ pub fn generate_watch_scripts( repos: &[String], io: &mut IoCtx<'_>, flags: RunFlags, -) -> Result<(), String> { +) -> Result { let lib_dirs: Vec = repos.iter().skip(1).map(|r| repo_dir_name(r)).collect(); if lib_dirs.is_empty() { - return Ok(()); + return Ok(false); } let ps1_lines: Vec = lib_dirs @@ -121,7 +121,7 @@ pub fn generate_watch_scripts( } } - Ok(()) + Ok(true) } /// Opens watch scripts in new terminals. diff --git a/src/commands/setup.rs b/src/commands/setup.rs index 737d110..d5adbcc 100644 --- a/src/commands/setup.rs +++ b/src/commands/setup.rs @@ -1,10 +1,5 @@ -use crate::{ - cli::{BuildSystem, ResolvedArgs}, - commands::build_project, - ctx::RunCtx, - utils::dry_run_or_do, -}; -use std::{fs, path::Path}; +use crate::{cli::ResolvedArgs, ctx::RunCtx, utils::dry_run_or_do}; +use std::fs; /// Prepares the build directory, optionally cleaning it first. /// # Errors @@ -47,21 +42,6 @@ pub fn prepare_build_dir( Ok(()) } -/// Detects the build system and runs configuration and optional build. -/// # Errors -/// Returns an error if detection or build fails. -pub fn configure_and_build( - args: &ResolvedArgs, - project_path: &Path, - build_path: &Path, - build_system: BuildSystem, - is_mono: bool, - ctx: &mut RunCtx<'_, '_>, -) -> Result<(), String> { - writeln!(ctx.io.output, "Configuring project").ok(); - build_project(args, build_path, project_path, build_system, is_mono, ctx) -} - /// Extracts and sanitizes the repository input from args. /// # Errors /// Returns an error if no repository is specified. diff --git a/src/commands/single.rs b/src/commands/single.rs index cc1eb7e..aaf7613 100644 --- a/src/commands/single.rs +++ b/src/commands/single.rs @@ -1,8 +1,6 @@ use crate::{ cli::{detect_build_system, BuildSystem, ResolvedArgs}, - commands::{ - configure_and_build, extract_repo_input, prepare_build_dir, print_mode_header, ModeHeader, - }, + commands::{build_project, extract_repo_input, prepare_build_dir, print_mode_header, ModeHeader}, ctx::RunCtx, repository::{clone_repository, repo_dir_name}, utils::dry_run::detect_or_dry_run, @@ -46,20 +44,15 @@ pub fn single_repo_mode( if let Some(build_system) = build_system { if build_system == BuildSystem::Npm { - configure_and_build(args, &repo_path, &repo_path, build_system, false, ctx)?; + build_project(args, &repo_path, &repo_path, build_system, false, ctx)?; } else { prepare_build_dir(&build_path, args.build.clean, ctx)?; - configure_and_build(args, &repo_path, &build_path, build_system, false, ctx)?; + build_project(args, &build_path, &repo_path, build_system, false, ctx)?; } } - if build_system.is_none() && ctx.flags.dry_run { - writeln!( - ctx.io.output, - "Would finish in {dir_name}/{}", - args.build.build_dir - ) - .ok(); + if build_system.is_none() { + writeln!(ctx.io.output, "Would finish in {dir_name}").ok(); } else if build_system == Some(BuildSystem::Npm) { writeln!(ctx.io.output, "Project finished in {dir_name}").ok(); } else { diff --git a/src/config/display.rs b/src/config/display.rs index f64ecc7..b0e16f2 100644 --- a/src/config/display.rs +++ b/src/config/display.rs @@ -45,7 +45,7 @@ pub fn list_configs(config: &SetupConfig, io: &mut IoCtx<'_>) { writeln!(io.output, " No configurations created.").ok(); writeln!( io.output, - " Run with --init-config to create a default configuration." + " Run with 'star-setup config init' to create a default configuration." ) .ok(); return; diff --git a/src/config/io.rs b/src/config/io.rs index d5c6ca2..fd02471 100644 --- a/src/config/io.rs +++ b/src/config/io.rs @@ -1,5 +1,9 @@ use crate::config::SetupConfig; -use std::{fs, io::{self, Write}, path::{Path, PathBuf}}; +use std::{ + fs, + io::{self, Write}, + path::{Path, PathBuf}, +}; /// Returns the list of paths to search for a config file. #[must_use] @@ -13,10 +17,13 @@ pub fn config_locations(path: &std::path::Path) -> Vec { .collect() } -fn io_error_msg(verb: &str, path: &Path, e: io::Error) -> String { +fn io_error_msg(verb: &str, path: &Path, e: &io::Error) -> String { match e.kind() { io::ErrorKind::PermissionDenied => format!("Error: No permission to {verb} {}", path.display()), - _ => format!("An unexpected error occurred: {verb} {}: {e}", path.display()), + _ => format!( + "An unexpected error occurred: {verb} {}: {e}", + path.display() + ), } } @@ -27,6 +34,10 @@ pub fn load_config( timing: bool, output: &mut impl Write, ) -> SetupConfig { + if verbose { + writeln!(output, "Loading config").ok(); + } + let mut invalid_count = 0; for path in locations { @@ -53,14 +64,16 @@ pub fn load_config( } }, Err(e) => { - writeln!(output, " {}", io_error_msg("read", path, e)).ok(); + writeln!(output, " {}", io_error_msg("read", path, &e)).ok(); invalid_count += 1; None } } }); - if let Some(config) = result { + if verbose || timing { + writeln!(output).ok(); + } return config; } } @@ -73,6 +86,9 @@ pub fn load_config( ) .ok(); } + if verbose || timing { + writeln!(output).ok(); + } SetupConfig::new() } @@ -97,7 +113,7 @@ pub fn save_config( serde_json::to_string_pretty(config).map_err(|e| format!("Failed to serialize config: {e}"))?; crate::time!(timing, output, "Write config", { - fs::write(&path, json).map_err(|e| io_error_msg("write to", &path, e))?; + fs::write(&path, json).map_err(|e| io_error_msg("write to", &path, &e))?; }); Ok(path) diff --git a/src/ctx.rs b/src/ctx.rs index 4c9683d..f50cd94 100644 --- a/src/ctx.rs +++ b/src/ctx.rs @@ -83,12 +83,14 @@ impl Runner for DryRunRunner { &mut self, cmd: &[&str], cwd: Option<&Path>, - _flags: RunFlags, + flags: RunFlags, output: &mut dyn Write, ) -> Result<(), String> { - writeln!(output, " Would run: {}", cmd.join(" ")).map_err(|e| e.to_string())?; - if let Some(dir) = cwd { - writeln!(output, " in directory: {}", dir.display()).map_err(|e| e.to_string())?; + if flags.verbose { + writeln!(output, " Would run: {}", cmd.join(" ")).map_err(|e| e.to_string())?; + if let Some(dir) = cwd { + writeln!(output, " in directory: {}", dir.display()).map_err(|e| e.to_string())?; + } } Ok(()) } diff --git a/src/interactive.rs b/src/interactive.rs index 331f7b4..54ddfa5 100644 --- a/src/interactive.rs +++ b/src/interactive.rs @@ -52,7 +52,6 @@ pub fn interactive_mode(args: &mut ResolvedArgs, io: &mut IoCtx<'_>) -> Result<( _ => {} } } - writeln!(io.output).ok(); } let build_type_str = ask_default(" Build type", args.build.build_type.to_cmake(), io)?; diff --git a/src/profile/crud.rs b/src/profile/crud.rs index 02a4553..30ea6a1 100644 --- a/src/profile/crud.rs +++ b/src/profile/crud.rs @@ -33,7 +33,7 @@ pub fn add_profile( flags: RunFlags, ) -> Result<(), String> { if args.len() < 2 { - return Err("--profile-add requires NAME REPO1 [REPO2 ...]".to_string()); + return Err("profile add requires NAME REPO1 [REPO2 ...]".to_string()); } let name = args[0].clone(); diff --git a/src/profile/display.rs b/src/profile/display.rs index 3ba59b4..02231de 100644 --- a/src/profile/display.rs +++ b/src/profile/display.rs @@ -19,7 +19,7 @@ pub fn list_profiles(config: &SetupConfig, io: &mut IoCtx<'_>) { if config.profiles.is_empty() { writeln!( io.output, - "No profiles configured. Run with --init-config to create a default configuration." + "No profiles configured. Run with profile add to create a new profile." ) .ok(); return; diff --git a/src/repository.rs b/src/repository.rs index 7aacb93..55d346a 100644 --- a/src/repository.rs +++ b/src/repository.rs @@ -64,7 +64,9 @@ pub fn clone_repository( ctx.flags, ctx.io.output, )?; - writeln!(ctx.io.output, " Finished cloning {repo_name}").ok(); + if ctx.flags.verbose { + writeln!(ctx.io.output, " Finished cloning {repo_name}").ok(); + } Ok::<(), String>(()) }) .map_err(|e| format!("Failed to clone {repo_path}: {e}"))?; diff --git a/src/run.rs b/src/run.rs index 0f2640d..87a048a 100644 --- a/src/run.rs +++ b/src/run.rs @@ -49,7 +49,7 @@ pub fn run(config_path: PathBuf) -> Result<(), Box> { Command::Profile(p) => { handle_profile_cmd(p.action, &mut config, yes, &mut io, flags)?; } - Command::Workspace(w) => handle_workspace_cmd(w.action, io, flags)?, + Command::Workspace(w) => handle_workspace_cmd(&w.action, io, flags)?, } return Ok(()); } diff --git a/src/utils/dry_run.rs b/src/utils/dry_run.rs index fe0ddcb..0b8d397 100644 --- a/src/utils/dry_run.rs +++ b/src/utils/dry_run.rs @@ -1,7 +1,6 @@ use crate::{cli::BuildSystem, ctx::RunCtx}; use std::path::Path; - /// Prints a dry-run message or executes `op`, timing it if not a dry run. /// # Errors /// Returns an error if `op` fails. @@ -14,11 +13,17 @@ pub fn dry_run_or_do( op: impl FnOnce() -> Result<(), String>, ) -> Result<(), String> { if ctx.flags.dry_run { - writeln!(ctx.io.output, " Would {verb}: {}", path.display()).ok(); + if ctx.flags.verbose { + writeln!(ctx.io.output, " Would {verb}: {}", path.display()).ok(); + } } else { - writeln!(ctx.io.output, " {progressive}: {}", path.display()).ok(); + if ctx.flags.verbose { + writeln!(ctx.io.output, " {progressive}: {}", path.display()).ok(); + } crate::time!(ctx.flags.timing, ctx.io.output, timer_label, { op() })?; - writeln!(ctx.io.output, " Done").ok(); + if ctx.flags.verbose { + writeln!(ctx.io.output, " Done").ok(); + } } Ok(()) } @@ -42,12 +47,16 @@ where } Some(bs) } else if ctx.flags.dry_run { - writeln!(ctx.io.output, " Would detect build system after cloning").ok(); + if ctx.flags.verbose { + writeln!(ctx.io.output, " Would detect build system after cloning").ok(); + } None } else { Some(detect_fn(ctx)?) }; - writeln!(ctx.io.output, " Finished detecting").ok(); + if ctx.flags.verbose { + writeln!(ctx.io.output, " Finished detecting").ok(); + } r }); writeln!(ctx.io.output).ok(); diff --git a/tests/commands/single.rs b/tests/commands/single.rs index 8d98c11..27ac090 100644 --- a/tests/commands/single.rs +++ b/tests/commands/single.rs @@ -74,12 +74,12 @@ fn test_single_repo_mode_dry_run_makes_no_fs_changes() { #[test] fn test_single_repo_mode_dry_run_clean_prints_would_remove() { let mut args = default_resolved(); - args.diagnostic.dry_run = true; args.build.clean = true; args.build.build_system = Some(BuildSystem::Cmake); let (_, output) = with_ctx_input(b"", DryRunRunner, |tmp_path, ctx| { ctx.flags.dry_run = true; + ctx.flags.verbose = true; single_repo_mode(&args, tmp_path, ctx).unwrap(); assert!(std::fs::read_dir(tmp_path).unwrap().next().is_none()); }); diff --git a/tests/ctx.rs b/tests/ctx.rs index 93395b0..8b3cd03 100644 --- a/tests/ctx.rs +++ b/tests/ctx.rs @@ -1,10 +1,17 @@ -use star_setup::ctx::{DryRunRunner, ProcessRunner, Runner}; +use star_setup::ctx::{DryRunRunner, ProcessRunner, RunFlags, Runner}; use std::path::Path; mod common; use common::{with_io, with_io_output}; use crate::common::make_flags; +fn verbose_flags() -> RunFlags { + RunFlags { + verbose: true, + ..make_flags() + } +} + #[test] fn test_process_runner_runs_command() { with_io(|io| { @@ -18,7 +25,7 @@ fn test_process_runner_runs_command() { fn test_dry_run_runner_prints_command() { let ((), output) = with_io_output(|io| { DryRunRunner - .run(&["git", "clone", "foo"], None, make_flags(), io.output) + .run(&["git", "clone", "foo"], None, verbose_flags(), io.output) .unwrap(); }); assert_eq!(output, " Would run: git clone foo\n"); @@ -31,7 +38,7 @@ fn test_dry_run_runner_prints_cwd() { .run( &["cmake", ".."], Some(Path::new("/tmp/build")), - make_flags(), + verbose_flags(), io.output, ) .unwrap(); diff --git a/tests/workspace/clean.rs b/tests/workspace/clean.rs index 7360a35..84dd0a0 100644 --- a/tests/workspace/clean.rs +++ b/tests/workspace/clean.rs @@ -29,6 +29,7 @@ fn test_workspace_clean_removes_build_dir() { fn test_workspace_clean_dry_run() { let (_, output) = with_ctx(MockRunner::new(), |tmp, ctx| { ctx.flags.dry_run = true; + ctx.flags.verbose = true; let ws = make_workspace(tmp, vec![]); fs::create_dir_all(&ws.build_path).unwrap(); From 9aee87150a921bbea93fd6eee14828ed418797b3 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 2 Jul 2026 23:45:48 -0400 Subject: [PATCH 16/22] fix: warn instead of silently returning on --clean for npm, remove node module removing in workspace clean --- src/commands/mono/mode.rs | 2 ++ src/commands/setup.rs | 8 ++++++-- src/commands/single.rs | 3 +++ src/workspace/clean.rs | 22 ++-------------------- 4 files changed, 13 insertions(+), 22 deletions(-) diff --git a/src/commands/mono/mode.rs b/src/commands/mono/mode.rs index efb58d6..f317618 100644 --- a/src/commands/mono/mode.rs +++ b/src/commands/mono/mode.rs @@ -85,6 +85,8 @@ pub fn mono_repo_mode( let map = generate_mono_config(bs, &mono_repo_path, &repos_path, &repo_dirs, &repos, ctx)?; if bs != BuildSystem::Npm { prepare_build_dir(build_path.as_path(), args.build.clean, ctx)?; + } else if args.build.clean && ctx.flags.verbose { + writeln!(ctx.io.output, " --clean has no effect for npm projects").ok(); } build_project(args, &build_path, &mono_repo_path, bs, true, ctx)?; map diff --git a/src/commands/setup.rs b/src/commands/setup.rs index d5adbcc..640fc28 100644 --- a/src/commands/setup.rs +++ b/src/commands/setup.rs @@ -25,7 +25,9 @@ pub fn prepare_build_dir( } }, )?; - writeln!(ctx.io.output, " Finished cleaning\n").ok(); + if ctx.flags.verbose { + writeln!(ctx.io.output, " Finished cleaning\n").ok(); + } } writeln!(ctx.io.output, "Creating build directory").ok(); @@ -38,7 +40,9 @@ pub fn prepare_build_dir( || fs::create_dir_all(build_path).map_err(|e| e.to_string()), )?; - writeln!(ctx.io.output, " Finished creating\n").ok(); + if ctx.flags.verbose { + writeln!(ctx.io.output, " Finished creating\n").ok(); + } Ok(()) } diff --git a/src/commands/single.rs b/src/commands/single.rs index aaf7613..1d6b0c4 100644 --- a/src/commands/single.rs +++ b/src/commands/single.rs @@ -44,6 +44,9 @@ pub fn single_repo_mode( if let Some(build_system) = build_system { if build_system == BuildSystem::Npm { + if args.build.clean && ctx.flags.verbose { + writeln!(ctx.io.output, " --clean has no effect for npm projects").ok(); + } build_project(args, &repo_path, &repo_path, build_system, false, ctx)?; } else { prepare_build_dir(&build_path, args.build.clean, ctx)?; diff --git a/src/workspace/clean.rs b/src/workspace/clean.rs index c999c2b..0b6009c 100644 --- a/src/workspace/clean.rs +++ b/src/workspace/clean.rs @@ -9,27 +9,9 @@ impl Workspace { writeln!(ctx.io.output, "Cleaning workspace").ok(); if self.root.join("package.json").exists() { - let node_modules = self.root.join("node_modules"); - if !node_modules.exists() { - writeln!( - ctx.io.output, - " node_modules does not exist: {}", - node_modules.display() - ) - .ok(); - return Ok(()); + if ctx.flags.verbose { + writeln!(ctx.io.output, " Clean has no effect for npm workspaces").ok(); } - dry_run_or_do( - "remove directory", - "Removing", - &node_modules, - ctx, - "Clean", - || { - fs::remove_dir_all(&node_modules) - .map_err(|e| format!("Failed to remove node_modules: {e}")) - }, - )?; return Ok(()); } From 5306a531248abc770590e4c9ed251ad89e013893 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 2 Jul 2026 23:49:44 -0400 Subject: [PATCH 17/22] docs: clarify run() error --- src/run.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/run.rs b/src/run.rs index 87a048a..08909e4 100644 --- a/src/run.rs +++ b/src/run.rs @@ -17,7 +17,9 @@ use std::{ /// Runs the setup process. /// # Errors -/// Returns an error if the configuration file is missing or corrupted. +/// Returns an error if arguments can't be resolved, +/// a required tool is missing, +/// or the selected mode fails. pub fn run(config_path: PathBuf) -> Result<(), Box> { let mut stdin = io::stdin().lock(); let mut stdout = io::stdout(); From e8bdaa87ac258704513e189b450d1783ee63b524 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Fri, 3 Jul 2026 02:21:54 -0400 Subject: [PATCH 18/22] fix: skip mono config/wrap/watch fs writes and process spawn on dry-run --- src/commands/mono/config.rs | 81 ++++++++++++++++++++++++++----------- src/commands/mono/mode.rs | 5 ++- src/commands/mono/watch.rs | 53 ++++++++++++++++++------ src/commands/mono/wraps.rs | 35 ++++++++++++---- src/commands/setup.rs | 6 ++- src/utils/dry_run.rs | 24 ++++++----- src/workspace/clean.rs | 3 +- tests/commands/mono/mode.rs | 25 ++++++++++++ 8 files changed, 174 insertions(+), 58 deletions(-) diff --git a/src/commands/mono/config.rs b/src/commands/mono/config.rs index 0487be9..52a90ac 100644 --- a/src/commands/mono/config.rs +++ b/src/commands/mono/config.rs @@ -1,6 +1,7 @@ use crate::{ ctx::{IoCtx, RunFlags}, repository::repo_dir_name, + utils::dry_run_or_do, }; use std::{fs, path::Path}; @@ -19,19 +20,38 @@ fn write_mono_repo_config( let content = render_template(&modules_str); let file_path = mono_dir.join(filename); - crate::time!(flags.timing, io.output, &format!("Generate {filename}"), { - fs::write(&file_path, content).map_err(|e| e.to_string())?; - }); + dry_run_or_do( + "create file", + "Creating", + &file_path, + io, + flags, + &format!("Generate {filename}"), + || fs::write(&file_path, content).map_err(|e| e.to_string()), + )?; // .to_string() is required to force an allocation and satisfy line coverage tracking - #[allow(clippy::to_string_in_format_args)] - writeln!( - io.output, - " Created root {} at {}\n", - filename.to_string(), - mono_dir.display() - ) - .ok(); + if flags.dry_run { + if flags.verbose { + #[allow(clippy::to_string_in_format_args)] + writeln!( + io.output, + " Would create root {} at {}\n", + filename.to_string(), + mono_dir.display() + ) + .ok(); + } + } else { + #[allow(clippy::to_string_in_format_args)] + writeln!( + io.output, + " Created root {} at {}\n", + filename.to_string(), + mono_dir.display() + ) + .ok(); + } Ok(()) } @@ -187,18 +207,33 @@ pub fn create_mono_repo_package_json( ); let file_path = mono_dir.join("package.json"); - crate::time!(flags.timing, io.output, "Generate package.json", { - fs::write(&file_path, content).map_err(|e| e.to_string())?; - }); - - #[allow(clippy::to_string_in_format_args)] - writeln!( - io.output, - " Created root {} at {}\n", - "package.json".to_string(), - mono_dir.display() - ) - .ok(); + dry_run_or_do( + "create file", + "Creating", + &file_path, + io, + flags, + "Generate package.json", + || fs::write(&file_path, content).map_err(|e| e.to_string()), + )?; + + if flags.dry_run { + if flags.verbose { + writeln!( + io.output, + " Would create root package.json at {}\n", + mono_dir.display() + ) + .ok(); + } + } else { + writeln!( + io.output, + " Created root package.json at {}\n", + mono_dir.display() + ) + .ok(); + } Ok(()) } diff --git a/src/commands/mono/mode.rs b/src/commands/mono/mode.rs index f317618..b7265a7 100644 --- a/src/commands/mono/mode.rs +++ b/src/commands/mono/mode.rs @@ -61,7 +61,8 @@ pub fn mono_repo_mode( "create directory", "Creating", &repos_path, - ctx, + &mut ctx.io, + ctx.flags, "Create directory", || fs::create_dir_all(&repos_path).map_err(|e| e.to_string()), )?; @@ -122,7 +123,7 @@ pub fn mono_repo_mode( ) }; - if build_system.is_none() { + if ctx.flags.dry_run || build_system.is_none() { writeln!( ctx.io.output, "Would finish setup in {}", diff --git a/src/commands/mono/watch.rs b/src/commands/mono/watch.rs index 25dad63..1c4e312 100644 --- a/src/commands/mono/watch.rs +++ b/src/commands/mono/watch.rs @@ -1,6 +1,7 @@ use crate::{ ctx::{IoCtx, RunFlags}, repository::repo_dir_name, + utils::dry_run_or_do, }; use std::{fs, path::Path}; @@ -97,20 +98,39 @@ pub fn generate_watch_scripts( sh_lines.join("\n") ); - crate::time!(flags.timing, io.output, "Write scripts", { - fs::write(mono_dir.join("watch.ps1"), ps1_content) - .map_err(|e| format!("Failed to write watch.ps1: {e}"))?; - fs::write(mono_dir.join("watch.sh"), sh_content) - .map_err(|e| format!("Failed to write watch.sh: {e}"))?; - Ok::<(), String>(()) - })?; + dry_run_or_do( + "write watch scripts", + "Writing", + mono_dir, + io, + flags, + "Write scripts", + || { + fs::write(mono_dir.join("watch.ps1"), ps1_content) + .map_err(|e| format!("Failed to write watch.ps1: {e}"))?; + fs::write(mono_dir.join("watch.sh"), sh_content) + .map_err(|e| format!("Failed to write watch.sh: {e}"))?; + Ok(()) + }, + )?; - writeln!( - io.output, - " Generated watch scripts at {}", - mono_dir.display() - ) - .ok(); + if flags.dry_run { + if flags.verbose { + writeln!( + io.output, + " Would generate watch scripts at {}", + mono_dir.display() + ) + .ok(); + } + } else { + writeln!( + io.output, + " Generated watch scripts at {}", + mono_dir.display() + ) + .ok(); + } if flags.verbose { writeln!(io.output, " Watching {} libraries:", lib_dirs.len()).ok(); @@ -132,6 +152,13 @@ pub fn open_watch_scripts( io: &mut IoCtx<'_>, flags: RunFlags, ) -> Result<(), String> { + if flags.dry_run { + if flags.verbose { + writeln!(io.output, " Would open watch scripts").ok(); + } + return Ok(()); + } + crate::time!(flags.timing, io.output, "Open", { #[cfg(target_os = "windows")] { diff --git a/src/commands/mono/wraps.rs b/src/commands/mono/wraps.rs index bbd0552..9379fe7 100644 --- a/src/commands/mono/wraps.rs +++ b/src/commands/mono/wraps.rs @@ -4,7 +4,10 @@ use std::{ path::{Path, PathBuf}, }; -use crate::ctx::{IoCtx, RunFlags}; +use crate::{ + ctx::{IoCtx, RunFlags}, + utils::dry_run_or_do, +}; /// Parses the `project()` name from `meson.build` content. /// Returns the name with hyphens replaced by underscores, or `None` if not found. @@ -116,12 +119,30 @@ pub fn hoist_wraps( format!("[wrap-file]\ndirectory = {dir_name}\n") }; let wrap_path = repos_dir.join(format!("{canonical_name}.wrap")); - fs::write(&wrap_path, &wrap_content).map_err(|e| e.to_string())?; - writeln!( - io.output, - " Generated wrap: {canonical_name}.wrap -> {dir_name}" - ) - .ok(); + dry_run_or_do( + "create file", + "Creating", + &wrap_path, + io, + flags, + "Write wrap", + || fs::write(&wrap_path, &wrap_content).map_err(|e| e.to_string()), + )?; + if flags.dry_run { + if flags.verbose { + writeln!( + io.output, + " Would generate wrap: {canonical_name}.wrap -> {dir_name}" + ) + .ok(); + } + } else { + writeln!( + io.output, + " Generated wrap: {canonical_name}.wrap -> {dir_name}" + ) + .ok(); + } } Ok(project_to_dir) diff --git a/src/commands/setup.rs b/src/commands/setup.rs index 640fc28..f4115ea 100644 --- a/src/commands/setup.rs +++ b/src/commands/setup.rs @@ -15,7 +15,8 @@ pub fn prepare_build_dir( "remove directory", "Removing", build_path, - ctx, + &mut ctx.io, + ctx.flags, "Clean", || { if build_path.exists() { @@ -35,7 +36,8 @@ pub fn prepare_build_dir( "create directory", "Creating", build_path, - ctx, + &mut ctx.io, + ctx.flags, "Create build directory", || fs::create_dir_all(build_path).map_err(|e| e.to_string()), )?; diff --git a/src/utils/dry_run.rs b/src/utils/dry_run.rs index 0b8d397..7afdbd8 100644 --- a/src/utils/dry_run.rs +++ b/src/utils/dry_run.rs @@ -1,4 +1,7 @@ -use crate::{cli::BuildSystem, ctx::RunCtx}; +use crate::{ + cli::BuildSystem, + ctx::{IoCtx, RunCtx, RunFlags}, +}; use std::path::Path; /// Prints a dry-run message or executes `op`, timing it if not a dry run. @@ -8,21 +11,22 @@ pub fn dry_run_or_do( verb: &str, progressive: &str, path: &Path, - ctx: &mut RunCtx, + io: &mut IoCtx<'_>, + flags: RunFlags, timer_label: &str, op: impl FnOnce() -> Result<(), String>, ) -> Result<(), String> { - if ctx.flags.dry_run { - if ctx.flags.verbose { - writeln!(ctx.io.output, " Would {verb}: {}", path.display()).ok(); + if flags.dry_run { + if flags.verbose { + writeln!(io.output, " Would {verb}: {}", path.display()).ok(); } } else { - if ctx.flags.verbose { - writeln!(ctx.io.output, " {progressive}: {}", path.display()).ok(); + if flags.verbose { + writeln!(io.output, " {progressive}: {}", path.display()).ok(); } - crate::time!(ctx.flags.timing, ctx.io.output, timer_label, { op() })?; - if ctx.flags.verbose { - writeln!(ctx.io.output, " Done").ok(); + crate::time!(flags.timing, io.output, timer_label, { op() })?; + if flags.verbose { + writeln!(io.output, " Done").ok(); } } Ok(()) diff --git a/src/workspace/clean.rs b/src/workspace/clean.rs index 0b6009c..d19b1eb 100644 --- a/src/workspace/clean.rs +++ b/src/workspace/clean.rs @@ -29,7 +29,8 @@ impl Workspace { "remove directory", "Removing", &self.build_path, - ctx, + &mut ctx.io, + ctx.flags, "Clean", || { fs::remove_dir_all(&self.build_path) diff --git a/tests/commands/mono/mode.rs b/tests/commands/mono/mode.rs index b2c9e9d..23a84cc 100644 --- a/tests/commands/mono/mode.rs +++ b/tests/commands/mono/mode.rs @@ -39,6 +39,31 @@ fn test_mono_repo_mode_dry_run_makes_no_fs_changes() { }); } +#[test] +fn test_mono_repo_mode_dry_run_with_build_system_makes_no_fs_changes() { + for bs in [ + star_setup::cli::BuildSystem::Npm, + star_setup::cli::BuildSystem::Cmake, + star_setup::cli::BuildSystem::Meson, + ] { + let mut args = default_resolved_mono(vec!["user/lib1".to_string()]); + args.diagnostic.dry_run = true; + args.build.build_system = Some(bs); + args.build.watch = true; + + with_ctx(DryRunRunner, |tmp_path, ctx| { + ctx.flags.dry_run = true; + + mono_repo_mode(&args, &SetupConfig::new(), tmp_path, ctx).unwrap(); + + assert!( + std::fs::read_dir(tmp_path).unwrap().next().is_none(), + "{bs:?} dry-run wrote to disk" + ); + }); + } +} + #[test] fn test_mono_repo_mode_with_build_system_flag() { let mut args = default_resolved_mono(vec!["user/lib1".to_string()]); From 1a3346706f99cee17c2340f825f5b5e7c4e34d3a Mon Sep 17 00:00:00 2001 From: Masonlet Date: Fri, 3 Jul 2026 02:43:17 -0400 Subject: [PATCH 19/22] fix: single-repo dry-run with forced build system falsely reports success --- src/commands/single.rs | 2 +- tests/commands/single.rs | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/commands/single.rs b/src/commands/single.rs index 1d6b0c4..13b9e93 100644 --- a/src/commands/single.rs +++ b/src/commands/single.rs @@ -54,7 +54,7 @@ pub fn single_repo_mode( } } - if build_system.is_none() { + if ctx.flags.dry_run || build_system.is_none() { writeln!(ctx.io.output, "Would finish in {dir_name}").ok(); } else if build_system == Some(BuildSystem::Npm) { writeln!(ctx.io.output, "Project finished in {dir_name}").ok(); diff --git a/tests/commands/single.rs b/tests/commands/single.rs index 27ac090..14d6770 100644 --- a/tests/commands/single.rs +++ b/tests/commands/single.rs @@ -88,6 +88,25 @@ fn test_single_repo_mode_dry_run_clean_prints_would_remove() { .contains("Would remove directory:")); } +#[test] +fn test_single_repo_mode_dry_run_with_build_system_says_would_finish() { + for bs in [BuildSystem::Npm, BuildSystem::Cmake, BuildSystem::Meson] { + let mut args = default_resolved(); + args.diagnostic.dry_run = true; + args.build.build_system = Some(bs); + + let (_, output) = with_ctx_input(b"", DryRunRunner, |tmp_path, ctx| { + ctx.flags.dry_run = true; + single_repo_mode(&args, tmp_path, ctx).unwrap(); + assert!(std::fs::read_dir(tmp_path).unwrap().next().is_none()); + }); + + let out = String::from_utf8(output).unwrap(); + assert!(out.contains("Would finish in"), "{bs:?}: {out}"); + assert!(!out.contains("Project finished"), "{bs:?}: {out}"); + } +} + #[test] fn test_single_repo_mode_with_build_system_flag() { let mut args = default_resolved(); From 6fe19307ab74ac1073625de7238c119569a3d63a Mon Sep 17 00:00:00 2001 From: Masonlet Date: Fri, 3 Jul 2026 02:46:03 -0400 Subject: [PATCH 20/22] fix: gate per-repo clone lines behind verbose in mono mode --- src/commands/mono/clone.rs | 22 +++++++++++++--------- tests/commands/mono/clone.rs | 23 ++++++++++++++++++++++- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/commands/mono/clone.rs b/src/commands/mono/clone.rs index 5a93d60..1a595fd 100644 --- a/src/commands/mono/clone.rs +++ b/src/commands/mono/clone.rs @@ -12,20 +12,24 @@ pub fn clone_mono_repos( writeln!(ctx.io.output, "Cloning repositories").ok(); crate::time!(ctx.flags.timing, ctx.io.output, "Clone", { for repo in repos { + if ctx.flags.verbose { + writeln!( + ctx.io.output, + " Cloning {}", + crate::repository::repo_dir_name(repo) + ) + .ok(); + } + clone_repository(repo, repos_path, ssh, true, false, ctx)?; + } + if ctx.flags.verbose { writeln!( ctx.io.output, - " Cloning {}", - crate::repository::repo_dir_name(repo) + " Finished cloning ({} repositories)", + repos.len() ) .ok(); - clone_repository(repo, repos_path, ssh, true, false, ctx)?; } - writeln!( - ctx.io.output, - " Finished cloning ({} repositories)", - repos.len() - ) - .ok(); Ok::<(), String>(()) })?; writeln!(ctx.io.output).ok(); diff --git a/tests/commands/mono/clone.rs b/tests/commands/mono/clone.rs index be4e54a..a7d7447 100644 --- a/tests/commands/mono/clone.rs +++ b/tests/commands/mono/clone.rs @@ -1,4 +1,4 @@ -use super::super::common::{with_ctx_runner, MockRunner}; +use super::super::common::{with_ctx, with_ctx_runner, MockRunner}; use star_setup::commands::mono::clone::clone_mono_repos; #[test] @@ -23,3 +23,24 @@ fn test_clone_mono_repos_empty() { assert!(runner.calls.is_empty()); } + +#[test] +fn test_clone_mono_repos_per_repo_lines_require_verbose() { + let repos = vec!["user/repo1".to_string(), "user/repo2".to_string()]; + + let (_, output) = with_ctx(MockRunner::new(), |tmp_path, ctx| { + clone_mono_repos(&repos, tmp_path, false, ctx).unwrap(); + }); + let out = String::from_utf8(output).unwrap(); + assert!(out.contains("Cloning repositories")); + assert!(!out.contains("Cloning user-repo1")); + assert!(!out.contains("Finished cloning (2 repositories)")); + + let (_, output) = with_ctx(MockRunner::new(), |tmp_path, ctx| { + ctx.flags.verbose = true; + clone_mono_repos(&repos, tmp_path, false, ctx).unwrap(); + }); + let out = String::from_utf8(output).unwrap(); + assert!(out.contains("Cloning user-repo1")); + assert!(out.contains("Finished cloning (2 repositories)")); +} From 51a86ce2c797f1156d954e9f6eca15da15793e0c Mon Sep 17 00:00:00 2001 From: Masonlet Date: Fri, 3 Jul 2026 02:50:03 -0400 Subject: [PATCH 21/22] fix: remove duplicate blank line from mono clone loop --- src/commands/single.rs | 1 + src/repository.rs | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/single.rs b/src/commands/single.rs index 13b9e93..46f3a0e 100644 --- a/src/commands/single.rs +++ b/src/commands/single.rs @@ -36,6 +36,7 @@ pub fn single_repo_mode( writeln!(ctx.io.output, "Cloning repository").ok(); clone_repository(repo, base_dir, args.connection.ssh, false, args.yes, ctx)?; + writeln!(ctx.io.output).ok(); let build_path = repo_path.join(&args.build.build_dir); let build_system = detect_or_dry_run(args.build.build_system, ctx, |ctx| { diff --git a/src/repository.rs b/src/repository.rs index 55d346a..e50b8d8 100644 --- a/src/repository.rs +++ b/src/repository.rs @@ -72,7 +72,6 @@ pub fn clone_repository( .map_err(|e| format!("Failed to clone {repo_path}: {e}"))?; } - writeln!(ctx.io.output).ok(); Ok(()) } From ed90cf5a24b76313e47ea0b99c2a772bad8c8170 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Fri, 3 Jul 2026 03:00:43 -0400 Subject: [PATCH 22/22] fix: print npm install header to avoid duplicate blank line before final message --- src/commands/build.rs | 1 + tests/commands/build.rs | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/commands/build.rs b/src/commands/build.rs index cadb08d..bb596af 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -97,6 +97,7 @@ pub fn npm_build( is_mono: bool, ctx: &mut RunCtx<'_, '_>, ) -> Result<(), String> { + writeln!(ctx.io.output, "Installing dependencies").ok(); crate::time!(ctx.flags.timing, ctx.io.output, "npm install", { ctx.runner.run( &["npm", "install"], diff --git a/tests/commands/build.rs b/tests/commands/build.rs index 9edb2bb..e323ab6 100644 --- a/tests/commands/build.rs +++ b/tests/commands/build.rs @@ -1,4 +1,4 @@ -use super::common::{default_resolved_with_no_build, with_ctx_runner, MockRunner}; +use super::common::{default_resolved_with_no_build, with_ctx, with_ctx_runner, MockRunner}; use star_setup::{ cli::BuildSystem, commands::{build_project, cmake_build, meson_build}, @@ -92,6 +92,16 @@ fn test_npm_build_with_build_step() { assert!(runner.calls[1].0.contains(&"build".to_string())); } +#[test] +fn test_npm_build_install_only_prints_header() { + let args = default_resolved_with_no_build(true); + let (_, output) = with_ctx(MockRunner::new(), |tmp_path, ctx| { + star_setup::commands::npm_build(&args, tmp_path, false, ctx).unwrap(); + }); + let out = String::from_utf8(output).unwrap(); + assert!(out.contains("Installing dependencies")); +} + #[test] fn test_build_project_dispatches_npm() { let args = default_resolved_with_no_build(true);