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"
diff --git a/README.md b/README.md
index a72dea2..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
@@ -86,7 +88,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
| Flag | Description |
@@ -123,13 +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).
-
### Mono-Repo Mode
Clones multiple repositories into a single workspace and auto-detects the build system.
@@ -141,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
```
@@ -167,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.
```
@@ -182,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.
```
@@ -209,12 +220,14 @@ 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
```
+
+
#### Workspace Mode
Manage an existing mono-repo workspace.
@@ -239,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)
@@ -277,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
diff --git a/src/cli/flags.rs b/src/cli/flags.rs
index d645fdb..6b56e9e 100644
--- a/src/cli/flags.rs
+++ b/src/cli/flags.rs
@@ -51,12 +51,19 @@ 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,
+
+ /// 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/src/cli/resolve.rs b/src/cli/resolve.rs
index 7a55811..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,6 +82,14 @@ 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/commands/dev.rs b/src/commands/dev.rs
new file mode 100644
index 0000000..e02de5d
--- /dev/null
+++ b/src/commands/dev.rs
@@ -0,0 +1,106 @@
+#[cfg(not(target_os = "windows"))]
+use crate::utils::process::resolve_exe_args;
+use crate::{
+ cli::{
+ BuildSystem::{self, Npm},
+ ResolvedArgs,
+ },
+ commands::read_package_json,
+ ctx::{IoCtx, RunCtx, RunFlags},
+};
+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();
+
+ #[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
+ .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 5eb2c1f..9a86894 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,7 @@ 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;
+pub use dev::{maybe_open_dev_server, open_dev_server, resolve_dev_command};
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/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/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/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/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/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..ca5d70f 100644
--- a/src/utils/process.rs
+++ b/src/utils/process.rs
@@ -8,52 +8,17 @@ 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.
+#[must_use]
+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 +31,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 +43,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 +103,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(),
+ )
+}
diff --git a/tests/cli/resolve.rs b/tests/cli/resolve.rs
index 8d50a7b..3631c4a 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,
}
}
@@ -235,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);
}
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"));
}
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/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,
},
);
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,