From 957c50672124036b77725a770a7767fa0f732f66 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Sat, 4 Jul 2026 10:13:47 -0400 Subject: [PATCH 01/13] fix: clarify docs and add conflicts_with --- src/cli/flags.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cli/flags.rs b/src/cli/flags.rs index d645fdb..47e8960 100644 --- a/src/cli/flags.rs +++ b/src/cli/flags.rs @@ -51,11 +51,11 @@ pub struct BuildFlags { #[arg(long = "meson-arg", action = Append)] pub meson_flags: Vec, - /// Automatically open watch scripts for npm mono-repo mode. - #[arg(long)] + /// Automatically open watch scripts for libraries if applicable. + #[arg(long, conflicts_with = "no_watch")] pub watch: bool, - /// Skip generating watch scripts for npm mono-repo mode. - #[arg(long)] + /// Skip generating watch scripts. (overrides config) + #[arg(long, conflicts_with = "watch")] pub no_watch: bool, } From 36584d64d433bbe914970f37f6c89893f5054ab0 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Sat, 4 Jul 2026 10:15:00 -0400 Subject: [PATCH 02/13] feat: add dev/no_dev flags --- README.md | 2 ++ src/cli/flags.rs | 7 +++++++ tests/common/args.rs | 2 ++ tests/config/types.rs | 4 ++++ 4 files changed, 15 insertions(+) diff --git a/README.md b/README.md index a72dea2..2a1a138 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,8 @@ cargo install --git https://github.com/star-setup/core | `--meson-arg ` | Pass additional argument to Meson | | `--watch` | Generate and open watch scripts (npm mono-repo mode) | | `--no-watch` | Skip generating watch scripts (npm mono-repo mode) | +| `--dev` | Automatically start the dev server (npm) | +| `--no-dev` | Skip opening the dev server (npm) | #### Mono-Repo diff --git a/src/cli/flags.rs b/src/cli/flags.rs index 47e8960..6b56e9e 100644 --- a/src/cli/flags.rs +++ b/src/cli/flags.rs @@ -57,6 +57,13 @@ pub struct BuildFlags { /// Skip generating watch scripts. (overrides config) #[arg(long, conflicts_with = "watch")] pub no_watch: bool, + + /// Automatically open dev server. + #[arg(long, conflicts_with = "no_dev")] + pub dev: bool, + /// Skip opening the dev server. (overrides config) + #[arg(long, conflicts_with = "dev")] + pub no_dev: bool, } #[derive(ClapArgs)] diff --git a/tests/common/args.rs b/tests/common/args.rs index f2e293d..c8152a0 100644 --- a/tests/common/args.rs +++ b/tests/common/args.rs @@ -38,6 +38,8 @@ pub fn default_args() -> Args { meson_flags: vec![], watch: false, no_watch: false, + dev: false, + no_dev: false, }, mono: MonoRepoFlags { mono_repo: false, diff --git a/tests/config/types.rs b/tests/config/types.rs index c74982d..0a66545 100644 --- a/tests/config/types.rs +++ b/tests/config/types.rs @@ -23,6 +23,8 @@ fn test_from_flags_defaults() { meson_flags: vec![], watch: false, no_watch: false, + dev: false, + no_dev: false, }; let mono = MonoRepoFlags { mono_repo: false, @@ -70,6 +72,8 @@ fn test_from_flags_with_values() { meson_flags: vec![], watch: false, no_watch: false, + dev: false, + no_dev: false, }; let mono = MonoRepoFlags { mono_repo: false, From 1c47f73f2ee1d52d2b6ce402512bc61b71763700 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Sat, 4 Jul 2026 10:18:47 -0400 Subject: [PATCH 03/13] feat: add dev/no_dev through resolved args --- README.md | 1 - src/cli/resolve.rs | 2 ++ src/cli/resolved.rs | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2a1a138..4376d36 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,6 @@ cargo install --git https://github.com/star-setup/core | `--dev` | Automatically start the dev server (npm) | | `--no-dev` | Skip opening the dev server (npm) | - #### Mono-Repo | Flag | Description | |------ |------------- | diff --git a/src/cli/resolve.rs b/src/cli/resolve.rs index 7a55811..104736a 100644 --- a/src/cli/resolve.rs +++ b/src/cli/resolve.rs @@ -108,6 +108,8 @@ pub fn resolve_with_config(mut args: Args, config: &SetupConfig) -> Result, pub watch: bool, pub no_watch: bool, + pub dev: bool, + pub no_dev: bool, } /// Resolved mono-repo flags after applying config and CLI overrides. From 9d2fcc0a1eee95b03a4b3ed9790b5e16a7437ad3 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Sat, 4 Jul 2026 12:46:30 -0400 Subject: [PATCH 04/13] refactor: lift read_package_json out of mono into shared commands module --- src/commands/mod.rs | 6 +++-- src/commands/mono/config.rs | 2 +- src/commands/mono/mod.rs | 2 -- src/commands/mono/watch.rs | 2 +- src/commands/{mono => }/npm.rs | 13 +++++----- tests/commands/mono/npm.rs | 44 ++++++++++++++-------------------- 6 files changed, 31 insertions(+), 38 deletions(-) rename src/commands/{mono => }/npm.rs (68%) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 5eb2c1f..6b6bac7 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -6,8 +6,7 @@ pub mod mono; pub use mono::{ build_repo_list, create_mono_repo_cmakelists, create_mono_repo_mesonbuild, create_mono_repo_package_json, generate_mono_config, generate_watch_scripts, hoist_wraps, - mono_repo_mode, read_package_json, resolve_repos_for_mono, resolve_setup_paths, - resolve_test_repo, + mono_repo_mode, resolve_repos_for_mono, resolve_setup_paths, resolve_test_repo, wraps::{parse_project_name, parse_provide_pairs}, }; pub mod single; @@ -16,3 +15,6 @@ pub mod setup; pub use setup::{extract_repo_input, prepare_build_dir}; pub mod handlers; pub use handlers::{handle_config_cmd, handle_profile_cmd, handle_workspace_cmd}; +pub mod npm; +pub use npm::read_package_json; +pub mod dev; diff --git a/src/commands/mono/config.rs b/src/commands/mono/config.rs index c4bd0e3..b62807a 100644 --- a/src/commands/mono/config.rs +++ b/src/commands/mono/config.rs @@ -154,7 +154,7 @@ pub fn create_mono_repo_package_json( if i == 0 { continue; } - if let Some(json) = read_package_json(repos_path, dir, "skipping override", io, flags) { + if let Some(json) = read_package_json(&repos_path.join(dir), "skipping override", io, flags) { if let Some(name) = json.get("name").and_then(|n| n.as_str()) { overrides.push(format!(" \"{name}\": \"*\"")); } diff --git a/src/commands/mono/mod.rs b/src/commands/mono/mod.rs index 3f10964..ed0aaa4 100644 --- a/src/commands/mono/mod.rs +++ b/src/commands/mono/mod.rs @@ -14,5 +14,3 @@ pub mod setup; pub use setup::{build_repo_list, generate_mono_config}; pub mod watch; pub use watch::{generate_watch_scripts, open_watch_scripts}; -pub mod npm; -pub use npm::read_package_json; diff --git a/src/commands/mono/watch.rs b/src/commands/mono/watch.rs index 849fb69..432cb05 100644 --- a/src/commands/mono/watch.rs +++ b/src/commands/mono/watch.rs @@ -15,7 +15,7 @@ fn get_watch_command( io: &mut IoCtx<'_>, flags: RunFlags, ) -> Option { - let json = read_package_json(repos_path, dir, "skipping", io, flags)?; + let json = read_package_json(&repos_path.join(dir), "skipping", io, flags)?; let scripts = json.get("scripts")?; if scripts.get("watch").is_some() { Some(format!("npm --workspace=repos/{dir} run watch")) diff --git a/src/commands/mono/npm.rs b/src/commands/npm.rs similarity index 68% rename from src/commands/mono/npm.rs rename to src/commands/npm.rs index 019bb1d..9292da6 100644 --- a/src/commands/mono/npm.rs +++ b/src/commands/npm.rs @@ -2,22 +2,22 @@ use crate::ctx::{IoCtx, RunFlags}; use serde_json::{from_str, Value}; use std::{fs::read_to_string, path::Path}; -/// Reads and parses `{repos_path}/{dir}/package.json`, warning (if verbose) and +/// Reads and parses `{repo_path}/package.json`, warning (if verbose) and /// returning `None` on I/O or parse failure. pub fn read_package_json( - repos_path: &Path, - dir: &str, + repo_path: &Path, skip_suffix: &str, io: &mut IoCtx<'_>, flags: RunFlags, ) -> Option { - let pkg_path = repos_path.join(dir).join("package.json"); + let pkg_path = repo_path.join("package.json"); match read_to_string(&pkg_path) { Err(_) => { if flags.verbose { writeln!( io.output, - " Warning: could not read {dir}/package.json, {skip_suffix}" + " Warning: could not read {}, {skip_suffix}", + pkg_path.display() ) .ok(); } @@ -28,7 +28,8 @@ pub fn read_package_json( if flags.verbose { writeln!( io.output, - " Warning: malformed {dir}/package.json, {skip_suffix}" + " Warning: malformed {}, {skip_suffix}", + pkg_path.display() ) .ok(); } diff --git a/tests/commands/mono/npm.rs b/tests/commands/mono/npm.rs index a7bcc51..9dd71b0 100644 --- a/tests/commands/mono/npm.rs +++ b/tests/commands/mono/npm.rs @@ -14,16 +14,11 @@ fn flags(verbose: bool) -> RunFlags { #[test] fn test_read_package_json_valid_returns_json_silently() { let tmp = TempDir::new().unwrap(); - let repos = tmp.path(); - create_dir_all(repos.join("user-lib1")).unwrap(); - write( - repos.join("user-lib1").join("package.json"), - r#"{"name": "@user/lib1"}"#, - ) - .unwrap(); + let repo = tmp.path().join("user-lib1"); + create_dir_all(&repo).unwrap(); + write(repo.join("package.json"), r#"{"name": "@user/lib1"}"#).unwrap(); - let (result, out) = - with_io_output(|io| read_package_json(repos, "user-lib1", "skipping", io, flags(true))); + let (result, out) = with_io_output(|io| read_package_json(&repo, "skipping", io, flags(true))); assert!(result.is_some()); assert_eq!(out, ""); } @@ -31,24 +26,20 @@ fn test_read_package_json_valid_returns_json_silently() { #[test] fn test_read_package_json_missing_file_warns_when_verbose() { let tmp = TempDir::new().unwrap(); - let (result, out) = with_io_output(|io| { - read_package_json( - tmp.path(), - "user-lib1", - "skipping override", - io, - flags(true), - ) - }); + let repo = tmp.path().join("user-lib1"); + let (result, out) = + with_io_output(|io| read_package_json(&repo, "skipping override", io, flags(true))); assert!(result.is_none()); - assert!(out.contains("could not read user-lib1/package.json, skipping override")); + let pkg = repo.join("package.json"); + assert!(out.contains(&pkg.display().to_string())); + assert!(out.contains("skipping override")); } #[test] fn test_read_package_json_missing_file_silent_when_quiet() { let tmp = TempDir::new().unwrap(); let (result, out) = - with_io_output(|io| read_package_json(tmp.path(), "user-lib1", "skipping", io, flags(false))); + with_io_output(|io| read_package_json(tmp.path(), "skipping", io, flags(false))); assert!(result.is_none()); assert_eq!(out, ""); } @@ -56,12 +47,13 @@ fn test_read_package_json_missing_file_silent_when_quiet() { #[test] fn test_read_package_json_malformed_warns_when_verbose() { let tmp = TempDir::new().unwrap(); - let repos = tmp.path(); - create_dir_all(repos.join("user-lib1")).unwrap(); - write(repos.join("user-lib1").join("package.json"), "{ not json").unwrap(); + let repo = tmp.path().join("user-lib1"); + create_dir_all(&repo).unwrap(); + write(repo.join("package.json"), "{ not json").unwrap(); - let (result, out) = - with_io_output(|io| read_package_json(repos, "user-lib1", "skipping", io, flags(true))); + let (result, out) = with_io_output(|io| read_package_json(&repo, "skipping", io, flags(true))); assert!(result.is_none()); - assert!(out.contains("malformed user-lib1/package.json, skipping")); + let pkg = repo.join("package.json"); + assert!(out.contains(&pkg.display().to_string())); + assert!(out.contains("skipping")); } From 0ec6577727a494bd0ab2785bfe7bc783fe32e473 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Sat, 4 Jul 2026 12:48:00 -0400 Subject: [PATCH 05/13] refactor: extract resolve_exe_args --- src/utils/mod.rs | 2 +- src/utils/process.rs | 126 +++++++++++++++++++++---------------------- 2 files changed, 63 insertions(+), 65 deletions(-) diff --git a/src/utils/mod.rs b/src/utils/mod.rs index c62b1be..f238cd1 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,7 +1,7 @@ pub mod prerequisites; pub use prerequisites::check_prerequisites; pub mod process; -pub use process::run_command; +pub use process::{resolve_exe_args, run_command}; pub mod dry_run; pub use dry_run::{detect_or_dry_run, dry_run_or_do, report_summary}; pub mod timing; diff --git a/src/utils/process.rs b/src/utils/process.rs index 86a4788..7bb19d1 100644 --- a/src/utils/process.rs +++ b/src/utils/process.rs @@ -8,52 +8,16 @@ use std::{ #[cfg(target_os = "windows")] use std::{collections::HashMap, path::PathBuf}; -/// Finds vcvars64.bat using vswhere.exe. -/// Returns None if vswhere is not found or no VS installation exists. -#[cfg(target_os = "windows")] -fn find_vcvars() -> Option { - let program_files = - var("ProgramFiles(x86)").unwrap_or_else(|_| r"C:\Program Files (x86)".to_string()); - let vswhere = PathBuf::from(program_files).join(r"Microsoft Visual Studio\Installer\vswhere.exe"); - - if !vswhere.exists() { - return None; +/// npm must be invoked via `cmd /c` on Windows; applies that prefix, else returns cmd unchanged. +pub fn resolve_exe_args<'a>(cmd: &[&'a str]) -> Vec<&'a str> { + #[cfg(target_os = "windows")] + if cmd.first() == Some(&"npm") { + return std::iter::once("cmd") + .chain(std::iter::once("/c")) + .chain(cmd.iter().copied()) + .collect(); } - - let output = Command::new(&vswhere) - .args(["-latest", "-property", "installationPath"]) - .output() - .ok()?; - - let install_path = String::from_utf8(output.stdout).ok()?; - let vcvars = PathBuf::from(install_path.trim()).join(r"VC\Auxiliary\Build\vcvars64.bat"); - - vcvars.exists().then_some(vcvars) -} - -/// Runs vcvars64.bat and captures the resulting environment variables. -/// # Errors -/// Returns an error if vcvars64.bat cannot be found or run. -#[cfg(target_os = "windows")] -fn get_msvc_env() -> Result, String> { - let vcvars = find_vcvars().ok_or("Could not find vcvars64.bat via vswhere")?; - let vcvars_str = vcvars.to_str().ok_or("Invalid vcvars path")?; - - let output = Command::new("cmd") - .args(["/c", vcvars_str, "&&", "set"]) - .output() - .map_err(|e| format!("Failed to run vcvars64.bat: {e}"))?; - - let stdout = String::from_utf8_lossy(&output.stdout); - Ok( - stdout - .lines() - .filter_map(|line| { - let (key, val) = line.split_once('=')?; - Some((key.to_string(), val.to_string())) - }) - .collect(), - ) + cmd.to_vec() } /// Runs a shell command with optional working directory. @@ -66,9 +30,9 @@ pub fn run_command( verbose: bool, output: &mut (impl Write + ?Sized), ) -> Result<(), String> { - let (exe, args) = match cmd { + let exe = match cmd { [] => return Err("No command provided".to_string()), - [exe, args @ ..] => (exe, args), + [exe, ..] => exe, }; if verbose { @@ -78,24 +42,10 @@ pub fn run_command( } } - #[cfg(target_os = "windows")] - let npm_cmd; - #[cfg(target_os = "windows")] - let (exe, args) = if cmd[0] == "npm" { - use std::iter::once; - - npm_cmd = once("cmd") - .chain(once("/c")) - .chain(cmd.iter().copied()) - .collect::>(); - (&npm_cmd[0], &npm_cmd[1..]) - } else { - (exe, args) - }; - - let mut command = Command::new(exe); + let resolved = resolve_exe_args(cmd); + let mut command = Command::new(resolved[0]); command.stdin(Stdio::null()); - command.args(args); + command.args(&resolved[1..]); if let Some(dir) = cwd { command.current_dir(dir); @@ -152,3 +102,51 @@ pub fn run_command( Ok(()) } + +/// Finds vcvars64.bat using vswhere.exe. +/// Returns None if vswhere is not found or no VS installation exists. +#[cfg(target_os = "windows")] +fn find_vcvars() -> Option { + let program_files = + var("ProgramFiles(x86)").unwrap_or_else(|_| r"C:\Program Files (x86)".to_string()); + let vswhere = PathBuf::from(program_files).join(r"Microsoft Visual Studio\Installer\vswhere.exe"); + + if !vswhere.exists() { + return None; + } + + let output = Command::new(&vswhere) + .args(["-latest", "-property", "installationPath"]) + .output() + .ok()?; + + let install_path = String::from_utf8(output.stdout).ok()?; + let vcvars = PathBuf::from(install_path.trim()).join(r"VC\Auxiliary\Build\vcvars64.bat"); + + vcvars.exists().then_some(vcvars) +} + +/// Runs vcvars64.bat and captures the resulting environment variables. +/// # Errors +/// Returns an error if vcvars64.bat cannot be found or run. +#[cfg(target_os = "windows")] +fn get_msvc_env() -> Result, String> { + let vcvars = find_vcvars().ok_or("Could not find vcvars64.bat via vswhere")?; + let vcvars_str = vcvars.to_str().ok_or("Invalid vcvars path")?; + + let output = Command::new("cmd") + .args(["/c", vcvars_str, "&&", "set"]) + .output() + .map_err(|e| format!("Failed to run vcvars64.bat: {e}"))?; + + let stdout = String::from_utf8_lossy(&output.stdout); + Ok( + stdout + .lines() + .filter_map(|line| { + let (key, val) = line.split_once('=')?; + Some((key.to_string(), val.to_string())) + }) + .collect(), + ) +} From 456bcfed4b062a80e5cf424fd43c2084a3a1fd26 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Sat, 4 Jul 2026 13:04:33 -0400 Subject: [PATCH 06/13] feat: wire dev server into single and mono modes --- src/commands/dev.rs | 94 +++++++++++++++++++++++++++++++++++++++ src/commands/mod.rs | 1 + src/commands/mono/mode.rs | 5 ++- src/commands/single.rs | 8 +++- src/utils/process.rs | 1 + 5 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 src/commands/dev.rs diff --git a/src/commands/dev.rs b/src/commands/dev.rs new file mode 100644 index 0000000..4d7e8aa --- /dev/null +++ b/src/commands/dev.rs @@ -0,0 +1,94 @@ +use crate::{ + cli::{ + BuildSystem::{self, Npm}, + ResolvedArgs, + }, + commands::read_package_json, + ctx::{IoCtx, RunCtx, RunFlags}, + utils::process::resolve_exe_args, +}; +use std::{ + path::Path, + process::{Command, Stdio}, +}; + +/// Returns the dev command for a repo, or `None` if it has no `dev` script. +pub fn resolve_dev_command( + repo_path: &Path, + io: &mut IoCtx<'_>, + flags: RunFlags, +) -> Option { + let json = read_package_json(repo_path, "skipping dev server", io, flags)?; + if json.get("scripts").and_then(|s| s.get("dev")).is_some() { + Some("npm run dev".to_string()) + } else { + if flags.verbose { + writeln!(io.output, " No dev script found, skipping dev server").ok(); + } + None + } +} + +/// Runs the dev server in the foreground, blocking until it exits (Ctrl-C). +/// # Errors +/// Returns an error only if the process fails to spawn. +pub fn open_dev_server( + repo_path: &Path, + cmd: &str, + io: &mut IoCtx<'_>, + flags: RunFlags, +) -> Result<(), String> { + if flags.dry_run { + writeln!(io.output, " Would start dev server: {cmd}").ok(); + return Ok(()); + } + + writeln!(io.output, "Starting dev server: {cmd}").ok(); + writeln!( + io.output, + " in {} (press Ctrl-C to stop)", + repo_path.display() + ) + .ok(); + + let parts: Vec<&str> = cmd.split_whitespace().collect(); + let resolved = resolve_exe_args(&parts); + let mut command = Command::new(resolved[0]); + command + .args(&resolved[1..]) + .current_dir(repo_path) + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()); + + match command.spawn() { + Err(e) => Err(format!("Failed to start dev server: {e}")), + Ok(mut child) => { + child.wait().ok(); + Ok(()) + } + } +} + +/// Opens the project's dev server if `--dev` was passed and the build system is npm. +/// # Errors +/// Returns an error only if the dev server process fails to spawn. +pub fn maybe_open_dev_server( + args: &ResolvedArgs, + build_system: Option, + repo_path: &Path, + ctx: &mut RunCtx<'_, '_>, +) -> Result<(), String> { + if !args.build.dev || build_system != Some(Npm) { + return Ok(()); + } + let cmd = if ctx.flags.dry_run { + Some("npm run dev".to_string()) + } else { + resolve_dev_command(repo_path, &mut ctx.io, ctx.flags) + }; + if let Some(cmd) = cmd { + open_dev_server(repo_path, &cmd, &mut ctx.io, ctx.flags)?; + } + Ok(()) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 6b6bac7..9a86894 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -18,3 +18,4 @@ pub use handlers::{handle_config_cmd, handle_profile_cmd, handle_workspace_cmd}; pub mod npm; pub use npm::read_package_json; pub mod dev; +pub use dev::{maybe_open_dev_server, open_dev_server, resolve_dev_command}; diff --git a/src/commands/mono/mode.rs b/src/commands/mono/mode.rs index 017e85e..2edf795 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::Npm, ResolvedArgs}, commands::{ - build_project, build_repo_list, extract_repo_input, + build_project, build_repo_list, extract_repo_input, maybe_open_dev_server, mono::{ display::{resolve_setup_paths, SetupPaths}, generate_mono_config, generate_watch_scripts, open_watch_scripts, print_setup_complete, @@ -133,5 +133,6 @@ pub fn mono_repo_mode( } else { print_setup_complete(&paths, total, &mut ctx.io, ctx.flags); } - Ok(()) + + maybe_open_dev_server(args, build_system, &repo_dirs[0], ctx) } diff --git a/src/commands/single.rs b/src/commands/single.rs index 9f4255b..89c5845 100644 --- a/src/commands/single.rs +++ b/src/commands/single.rs @@ -1,6 +1,9 @@ use crate::{ cli::{detect_build_system, BuildSystem::Npm, ResolvedArgs}, - commands::{build_project, extract_repo_input, prepare_build_dir, print_mode_header, ModeHeader}, + commands::{ + build_project, extract_repo_input, maybe_open_dev_server, prepare_build_dir, print_mode_header, + ModeHeader, + }, ctx::RunCtx, prompts::confirm, repository::{ @@ -87,5 +90,6 @@ pub fn single_repo_mode( if ctx.flags.timing { writeln!(ctx.io.output, "[timing] Total: {:.2?}", total.elapsed()).ok(); } - Ok(()) + + maybe_open_dev_server(args, build_system, &repo_path, ctx) } diff --git a/src/utils/process.rs b/src/utils/process.rs index 7bb19d1..ca5d70f 100644 --- a/src/utils/process.rs +++ b/src/utils/process.rs @@ -9,6 +9,7 @@ use std::{ use std::{collections::HashMap, path::PathBuf}; /// npm must be invoked via `cmd /c` on Windows; applies that prefix, else returns cmd unchanged. +#[must_use] pub fn resolve_exe_args<'a>(cmd: &[&'a str]) -> Vec<&'a str> { #[cfg(target_os = "windows")] if cmd.first() == Some(&"npm") { From b936614d0056ee391a2241fcd28ed6cbe674cd4d Mon Sep 17 00:00:00 2001 From: Masonlet Date: Sat, 4 Jul 2026 13:19:56 -0400 Subject: [PATCH 07/13] refactor: make watch/dev flags config-aware --- src/cli/resolve.rs | 32 ++++++++++++++++++++++++++++---- src/cli/resolved.rs | 4 ++-- src/config/crud.rs | 4 ++++ src/config/display.rs | 4 ++++ src/config/types.rs | 16 ++++++++++++++++ tests/cli/resolve.rs | 4 ++++ tests/config/crud.rs | 4 ++++ tests/config/fixtures.rs | 4 ++++ tests/config/io.rs | 4 ++++ 9 files changed, 70 insertions(+), 6 deletions(-) diff --git a/src/cli/resolve.rs b/src/cli/resolve.rs index 104736a..170dfc2 100644 --- a/src/cli/resolve.rs +++ b/src/cli/resolve.rs @@ -69,6 +69,30 @@ pub fn resolve_with_config(mut args: Args, config: &SetupConfig) -> Result Result, pub no_build: bool, pub clean: bool, - pub cmake_flags: Vec, - pub meson_flags: Vec, pub watch: bool, pub no_watch: bool, pub dev: bool, pub no_dev: bool, + pub cmake_flags: Vec, + pub meson_flags: Vec, } /// Resolved mono-repo flags after applying config and CLI overrides. diff --git a/src/config/crud.rs b/src/config/crud.rs index dd9cfae..90daaa5 100644 --- a/src/config/crud.rs +++ b/src/config/crud.rs @@ -66,6 +66,10 @@ pub fn create_default_config( verbose: false, timing: false, dry_run: false, + watch: false, + no_watch: false, + dev: false, + no_dev: false, cmake_flags: vec![], meson_flags: vec![], }, diff --git a/src/config/display.rs b/src/config/display.rs index b0e16f2..7d0e376 100644 --- a/src/config/display.rs +++ b/src/config/display.rs @@ -16,6 +16,10 @@ pub fn format_entry(e: &ConfigEntry) -> String { writeln!(out, " Clean flag: {}", e.clean).ok(); writeln!(out, " Verbose flag: {}", e.verbose).ok(); writeln!(out, " Timing flag: {}", e.timing).ok(); + writeln!(out, " Watch flag: {}", e.watch).ok(); + writeln!(out, " No-watch flag: {}", e.no_watch).ok(); + writeln!(out, " Dev flag: {}", e.dev).ok(); + writeln!(out, " No-dev flag: {}", e.no_dev).ok(); if e.cmake_flags.is_empty() { out.push('\n'); } else if e.cmake_flags.len() == 1 { diff --git a/src/config/types.rs b/src/config/types.rs index 5b3e8d4..80a02c0 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -26,6 +26,14 @@ pub struct ConfigEntry { pub timing: bool, /// Print commands instead of executing them. pub dry_run: bool, + /// Automatically open the libraries watch scripts. + pub watch: bool, + /// Do not generate the libraries watch scripts. + pub no_watch: bool, + /// Automatically open the test repo's dev server. + pub dev: bool, + /// Do not open the test repo's dev server. + pub no_dev: bool, /// Additional `CMake` arguments. pub cmake_flags: Vec, /// Additional `Meson` arguments. @@ -62,6 +70,10 @@ impl ConfigEntry { verbose: diagnostic.verbose, timing: diagnostic.timing, dry_run: diagnostic.dry_run, + watch: build.watch, + no_watch: build.no_watch, + dev: build.dev, + no_dev: build.no_dev, cmake_flags: build.cmake_flags.clone(), meson_flags: build.meson_flags.clone(), } @@ -80,6 +92,10 @@ impl From<&ResolvedArgs> for ConfigEntry { verbose: args.diagnostic.verbose, timing: args.diagnostic.timing, dry_run: args.diagnostic.dry_run, + watch: args.build.watch, + no_watch: args.build.no_watch, + dev: args.build.dev, + no_dev: args.build.no_dev, cmake_flags: args.build.cmake_flags.clone(), meson_flags: args.build.meson_flags.clone(), } diff --git a/tests/cli/resolve.rs b/tests/cli/resolve.rs index 8d50a7b..2f96222 100644 --- a/tests/cli/resolve.rs +++ b/tests/cli/resolve.rs @@ -19,6 +19,10 @@ fn create_test_config_entry() -> ConfigEntry { dry_run: false, cmake_flags: vec![], meson_flags: vec![], + watch: false, + no_watch: false, + dev: false, + no_dev: false, } } diff --git a/tests/config/crud.rs b/tests/config/crud.rs index cb54f72..4877d98 100644 --- a/tests/config/crud.rs +++ b/tests/config/crud.rs @@ -64,6 +64,10 @@ fn test_add_config_aborts_when_exists_and_not_confirmed() { dry_run: false, cmake_flags: vec![], meson_flags: vec![], + watch: false, + no_watch: false, + dev: false, + no_dev: false, }, false, io, diff --git a/tests/config/fixtures.rs b/tests/config/fixtures.rs index 7fcc811..c40ebd5 100644 --- a/tests/config/fixtures.rs +++ b/tests/config/fixtures.rs @@ -13,5 +13,9 @@ pub fn sample_entry() -> ConfigEntry { dry_run: false, cmake_flags: vec![], meson_flags: vec![], + watch: false, + no_watch: false, + dev: false, + no_dev: false, } } diff --git a/tests/config/io.rs b/tests/config/io.rs index d4b0b74..8c9d346 100644 --- a/tests/config/io.rs +++ b/tests/config/io.rs @@ -28,6 +28,10 @@ fn test_save_and_load_roundtrip() { dry_run: false, cmake_flags: vec![], meson_flags: vec![], + watch: false, + no_watch: false, + dev: false, + no_dev: false, }, ); From 07b03e9dffde48fc4f35c8bc81079bea11e84830 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Sat, 4 Jul 2026 13:28:44 -0400 Subject: [PATCH 08/13] test: add coverage for watch/dev config --- tests/cli/resolve.rs | 76 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 13 deletions(-) diff --git a/tests/cli/resolve.rs b/tests/cli/resolve.rs index 2f96222..3631c4a 100644 --- a/tests/cli/resolve.rs +++ b/tests/cli/resolve.rs @@ -239,20 +239,70 @@ fn test_resolve_with_config_negative_flags_override_config() { args.build.no_clean = true; let resolved = resolve_with_config(args, &config).unwrap(); - assert!( - !resolved.connection.ssh, - "https should override config ssh:true" - ); - assert!( - !resolved.diagnostic.verbose, - "no_verbose should override config verbose:true" + assert!(!resolved.connection.ssh); + assert!(!resolved.diagnostic.verbose); + assert!(!resolved.build.no_build); + assert!(!resolved.build.clean); +} + +#[test] +fn test_resolve_with_config_watch_dev_defaults_from_config() { + let config = config_with_entry( + "default", + ConfigEntry { + watch: true, + dev: true, + ..create_test_config_entry() + }, ); - assert!( - !resolved.build.no_build, - "build should override config no_build:true" + + let resolved = resolve_with_config(default_args(), &config).unwrap(); + assert!(resolved.build.watch); + assert!(resolved.build.dev); + assert!(!resolved.build.no_watch); + assert!(!resolved.build.no_dev); +} + +#[test] +fn test_resolve_with_config_cli_negatives_override_watch_dev_config() { + let config = config_with_entry( + "default", + ConfigEntry { + watch: true, + dev: true, + ..create_test_config_entry() + }, ); - assert!( - !resolved.build.clean, - "no_clean should override config clean:true" + + let mut args = default_args(); + args.build.no_watch = true; + args.build.no_dev = true; + + let resolved = resolve_with_config(args, &config).unwrap(); + assert!(!resolved.build.watch); + assert!(!resolved.build.dev); + assert!(resolved.build.no_watch); + assert!(resolved.build.no_dev); +} + +#[test] +fn test_resolve_with_config_cli_positives_override_no_watch_no_dev_config() { + let config = config_with_entry( + "default", + ConfigEntry { + no_watch: true, + no_dev: true, + ..create_test_config_entry() + }, ); + + let mut args = default_args(); + args.build.watch = true; + args.build.dev = true; + + let resolved = resolve_with_config(args, &config).unwrap(); + assert!(resolved.build.watch); + assert!(resolved.build.dev); + assert!(!resolved.build.no_watch); + assert!(!resolved.build.no_dev); } From d94ad58e170379eef2318d329c2ae18611c4d1c2 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Sat, 4 Jul 2026 13:31:19 -0400 Subject: [PATCH 09/13] chore: bump v0.4.5 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index dd282c2..4cf84ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "star-setup" -version = "0.4.4" +version = "0.4.5" edition = "2021" repository = "https://github.com/star-setup/core" description = "Lightweight CLI to clone, configure, and wire single or multi-repo ecosystems" From 6b8ff50e3a9fdfb13452b3649e7e1f9068435035 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Sat, 4 Jul 2026 13:34:15 -0400 Subject: [PATCH 10/13] docs: document --dev flag usage --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4376d36..26beaf7 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,8 @@ star-setup username/repo Build system is auto-detected from the repository root (`CMakeLists.txt` → CMake, `meson.build` → Meson, `package.json` → npm). +Pass `--dev` to launch the project's dev server after setup — runs in the foreground; press Ctrl-C to stop. + ### Mono-Repo Mode Clones multiple repositories into a single workspace and auto-detects the build system. @@ -210,7 +212,7 @@ star-setup username/repo --repos user/lib1 user/lib2 star-setup username/repo --repos user/lib1 user/lib2 --no-watch ``` -After setup, run the game from its repo directory: +After setup pass `--dev` to launch it automatically, or run the game from its repo directory: ```bash cd build-mono/repos/user-my-repo npm run dev From 67dab247358227c466e7590f4776db68dab65bf2 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Sat, 4 Jul 2026 13:46:17 -0400 Subject: [PATCH 11/13] docs: collapse installation and workspace structure sections --- README.md | 58 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 26beaf7..cc83620 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,9 @@ star-setup username/repo --repos user/lib1 user/lib2 - npm (Node.js 22+) ## Installation +Download the latest binary from [Releases](https://github.com/star-setup/core/releases), or use one of the methods below. -Download the latest binary from [Releases](https://github.com/star-setup/core/releases), or: +
Show installation methods ### Homebrew (macOS/Linux) ```bash @@ -64,6 +65,7 @@ Download the `.msi` from [Releases](https://github.com/star-setup/core/releases) ```bash cargo install --git https://github.com/star-setup/core ``` +
## Usage @@ -124,15 +126,13 @@ Interactive mode complete ``` ### Single Repository Mode +Build system is auto-detected from the repository root (`CMakeLists.txt` → CMake, `meson.build` → Meson, `package.json` → npm). Pass `--dev` to launch the project's dev server after setup. + ```bash # Clone and build using a single repository star-setup username/repo ``` -Build system is auto-detected from the repository root (`CMakeLists.txt` → CMake, `meson.build` → Meson, `package.json` → npm). - -Pass `--dev` to launch the project's dev server after setup — runs in the foreground; press Ctrl-C to stop. - ### Mono-Repo Mode Clones multiple repositories into a single workspace and auto-detects the build system. @@ -144,7 +144,9 @@ star-setup username/repo --repos user/lib1 user/lib2 star-setup username/repo --profile myprofile ``` -#### Workspace Structure (CMake) +#### Workspace Structures +
CMake + Generates a root `CMakeLists.txt` wiring all repositories as subdirectories ``` @@ -170,7 +172,10 @@ endif() ``` This allows the same repository to work both standalone (fetching dependencies automatically) and inside a mono-repo workspace (linking locally for full cross-module debugging). -#### Workspace Structure (Meson) +
+ +
Meson + Generates a root `meson.build` and auto-generates local `.wrap` files bridging canonical dependency names to cloned directories. ``` @@ -185,7 +190,10 @@ build-mono/ └── build/ # Build output ``` -#### Workspace Structure (Npm) +
+ +
Npm + Generates a workspace root `package.json` that wires all cloned repositories together using npm workspaces and forces local resolution via `overrides` — no changes to individual repository files. Each lib's `package.json` is read after cloning to extract its package name for the overrides map. ``` @@ -218,6 +226,8 @@ cd build-mono/repos/user-my-repo npm run dev ``` +
+ #### Workspace Mode Manage an existing mono-repo workspace. @@ -242,22 +252,6 @@ Workspace flags: | `--mono-dir ` | Workspace directory name (default: `build-mono`) | | `--build-dir ` | Build directory name (default: `build`) | -### Profile Mode -Profiles represent a saved ecosystem of libraries commonly used together. -```bash -# Add a profile -star-setup profile add myprofile user/lib1 user/lib2 - -# List profiles -star-setup profile list - -# Remove a profile -star-setup profile remove myprofile - -# Use a profile -star-setup username/repo --profile myprofile -``` - ### Config Mode Config files are checked in this order: - `./.star-setup.json` (current directory) @@ -280,6 +274,22 @@ star-setup config remove myconfig star-setup username/repo --config myconfig ``` +### Profile Mode +Profiles represent a saved ecosystem of libraries commonly used together. +```bash +# Add a profile +star-setup profile add myprofile user/lib1 user/lib2 + +# List profiles +star-setup profile list + +# Remove a profile +star-setup profile remove myprofile + +# Use a profile +star-setup username/repo --profile myprofile +``` + ### Development ```bash git clone https://github.com/star-setup/core From f64bb0579e9ff8273acd81aadea506327c884bf7 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Sat, 4 Jul 2026 14:21:19 -0400 Subject: [PATCH 12/13] fix: use PowerShell for dev server on Windows to avoid batch-termination prompt --- src/commands/dev.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/commands/dev.rs b/src/commands/dev.rs index 4d7e8aa..e02de5d 100644 --- a/src/commands/dev.rs +++ b/src/commands/dev.rs @@ -1,3 +1,5 @@ +#[cfg(not(target_os = "windows"))] +use crate::utils::process::resolve_exe_args; use crate::{ cli::{ BuildSystem::{self, Npm}, @@ -5,7 +7,6 @@ use crate::{ }, commands::read_package_json, ctx::{IoCtx, RunCtx, RunFlags}, - utils::process::resolve_exe_args, }; use std::{ path::Path, @@ -51,11 +52,22 @@ pub fn open_dev_server( ) .ok(); - let parts: Vec<&str> = cmd.split_whitespace().collect(); - let resolved = resolve_exe_args(&parts); - let mut command = Command::new(resolved[0]); + #[cfg(target_os = "windows")] + let mut command = { + let mut c = Command::new("powershell"); + c.args(["-NoProfile", "-Command", cmd]); + c + }; + #[cfg(not(target_os = "windows"))] + let mut command = { + let parts: Vec<&str> = cmd.split_whitespace().collect(); + let resolved = resolve_exe_args(&parts); + let mut c = Command::new(resolved[0]); + c.args(&resolved[1..]); + c + }; + command - .args(&resolved[1..]) .current_dir(repo_path) .stdin(Stdio::inherit()) .stdout(Stdio::inherit()) From ed4aca89c3a8775480ca81ffdd93f3874955a7fc Mon Sep 17 00:00:00 2001 From: Masonlet Date: Sat, 4 Jul 2026 14:27:36 -0400 Subject: [PATCH 13/13] refactor: extract resolve_flag_pair to shrink resolve_with_config --- src/cli/resolve.rs | 41 +++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/src/cli/resolve.rs b/src/cli/resolve.rs index 170dfc2..22cdcab 100644 --- a/src/cli/resolve.rs +++ b/src/cli/resolve.rs @@ -22,6 +22,19 @@ pub fn resolve_bool(positive: bool, negative: bool, config: Option, defaul } } +/// Resolves a positive/negative flag pair, each using the other as its negation. +fn resolve_flag_pair( + pos: bool, + neg: bool, + cfg_pos: Option, + cfg_neg: Option, +) -> (bool, bool) { + ( + resolve_bool(pos, neg, cfg_pos, false), + resolve_bool(neg, pos, cfg_neg, false), + ) +} + /// Resolves raw `Args` into `ResolvedArgs` by applying config defaults and CLI overrides. /// # Errors /// Returns an error if the named config does not exist in the provided `SetupConfig`. @@ -69,29 +82,13 @@ pub fn resolve_with_config(mut args: Args, config: &SetupConfig) -> Result