From 7b69bf520ea99138a87359d17c6581b3eb5d3994 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Tue, 11 Aug 2026 20:59:54 -0400 Subject: [PATCH 1/4] feat: add --test-repos overriding profile test repos --- src/cli/args.rs | 4 +-- src/cli/commands.rs | 1 + src/cli/flags.rs | 8 +++-- src/commands/mono/resolve.rs | 13 +++++--- src/commands/setup.rs | 5 +-- src/interactive.rs | 8 +++-- src/resolve/resolved.rs | 2 +- src/resolve/resolver.rs | 56 +++++++++++++++++++--------------- src/run.rs | 2 +- tests/commands/mono/mode.rs | 2 +- tests/commands/mono/resolve.rs | 5 +-- tests/common/args.rs | 1 + tests/config/types.rs | 2 ++ tests/interactive.rs | 8 ++--- 14 files changed, 71 insertions(+), 46 deletions(-) diff --git a/src/cli/args.rs b/src/cli/args.rs index b2c2a97..8d8a787 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -27,8 +27,8 @@ pub enum Command { long_about = None, )] pub struct Args { - /// Repository name (username/repo) or full GitHub URL - #[arg(conflicts_with = "profile")] + /// Test repository (username/repo or URL) + #[arg(conflicts_with = "test_repos")] pub repo: Option, /// Select a named configuration to use diff --git a/src/cli/commands.rs b/src/cli/commands.rs index 2f82e2f..6a39207 100644 --- a/src/cli/commands.rs +++ b/src/cli/commands.rs @@ -12,6 +12,7 @@ pub struct ConfigCommand { /// Config subcommand actions. #[derive(Subcommand)] +#[allow(clippy::large_enum_variant)] pub enum ConfigAction { /// Create a default config file in the current directory. Init, diff --git a/src/cli/flags.rs b/src/cli/flags.rs index abe5e92..f40eb63 100644 --- a/src/cli/flags.rs +++ b/src/cli/flags.rs @@ -73,11 +73,15 @@ pub struct MonoRepoFlags { #[arg(long)] pub mono_repo: bool, - /// Directory name for mono-repo cloning + /// Directory name #[arg(long)] pub mono_dir: Option, - /// List of library dependencies to clone in mono-repo mode + /// List of test repositories to clone + #[arg(long, num_args = 1..)] + pub test_repos: Option>, + + /// List of library dependencies to clone #[arg(long, num_args = 1.., conflicts_with = "profile")] pub deps: Option>, diff --git a/src/commands/mono/resolve.rs b/src/commands/mono/resolve.rs index a05000c..98a404b 100644 --- a/src/commands/mono/resolve.rs +++ b/src/commands/mono/resolve.rs @@ -42,6 +42,14 @@ pub fn resolve_test_repos_for_mono( args: &ResolvedArgs, profile: Option<&Profile>, ) -> Result, String> { + if !args.mono.test_repos.is_empty() { + return args + .mono + .test_repos + .iter() + .map(|r| resolve_test_repo(r.trim_end_matches('/'))) + .collect(); + } if let Some(p) = profile { return p .test_repos @@ -49,10 +57,7 @@ pub fn resolve_test_repos_for_mono( .map(|r| resolve_test_repo(r.trim_end_matches('/'))) .collect(); } - match args.repo.as_deref() { - Some(r) => resolve_test_repo(r.trim_end_matches('/')).map(|r| vec![r]), - None => Err("No repository specified".to_string()), - } + Err("No repository specified".to_string()) } /// Resolves the dependency repositories for mono-repo mode from a profile or explicit list, deduplicating deps shared across test repos. diff --git a/src/commands/setup.rs b/src/commands/setup.rs index 618245a..5621b37 100644 --- a/src/commands/setup.rs +++ b/src/commands/setup.rs @@ -56,8 +56,9 @@ pub fn prepare_build_dir( /// Returns an error if no repository is specified. pub fn extract_repo_input(args: &ResolvedArgs) -> Result<&str, String> { args - .repo - .as_deref() + .mono + .test_repos + .first() .map(|r| r.trim_end_matches('/')) .ok_or_else(|| "No repository specified".to_string()) } diff --git a/src/interactive.rs b/src/interactive.rs index 42a456a..b275d62 100644 --- a/src/interactive.rs +++ b/src/interactive.rs @@ -11,8 +11,12 @@ use crate::{ pub fn interactive_mode(args: &mut ResolvedArgs, io: &mut IoCtx<'_>) -> Result<(), String> { 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)?); + if args.mono.test_repos.is_empty() { + let input = ask_required( + " Enter test repositories (space separated user/repo or URL)", + io, + )?; + args.mono.test_repos = input.split_whitespace().map(String::from).collect(); } args.connection.ssh = ask_bool_if(" Use SSH?", args.connection.ssh, io)?; diff --git a/src/resolve/resolved.rs b/src/resolve/resolved.rs index 28d1012..51f3139 100644 --- a/src/resolve/resolved.rs +++ b/src/resolve/resolved.rs @@ -31,6 +31,7 @@ pub struct ResolvedBuildFlags { pub struct ResolvedMonoFlags { pub mono_repo: bool, pub mono_dir: String, + pub test_repos: Vec, pub deps: Option>, pub profile: Option, } @@ -38,7 +39,6 @@ pub struct ResolvedMonoFlags { /// Fully resolved arguments ready for command execution. #[derive(Debug)] pub struct ResolvedArgs { - pub repo: Option, pub yes: bool, pub connection: ResolvedConnectionFlags, pub diagnostic: RunFlags, diff --git a/src/resolve/resolver.rs b/src/resolve/resolver.rs index 27a45b3..28abe91 100644 --- a/src/resolve/resolver.rs +++ b/src/resolve/resolver.rs @@ -96,30 +96,10 @@ fn resolve_build_flags( }) } -fn resolve_mono_flags(mono: MonoRepoFlags, default: Option<&ConfigEntry>) -> ResolvedMonoFlags { - let deps = mono.deps; - let profile = mono.profile; - let mono_repo = mono.mono_repo || deps.is_some() || profile.is_some(); - ResolvedMonoFlags { - mono_repo, - mono_dir: mono - .mono_dir - .or_else(|| default.map(|e| e.mono_dir.clone())) - .unwrap_or_else(|| "build-mono".to_string()), - deps, - profile, - } -} - -/// Fills the repo from the named profile's `test_repo` when no positional repo -/// was given, so all downstream code sees one resolved repo value. +/// Verifies the named profile exists, if one was requested. /// # Errors /// Returns an error if `profile` names a profile that does not exist. -fn resolve_repo( - repo: Option, - profile: Option<&str>, - config: &Config, -) -> Result, String> { +fn validate_profile(profile: Option<&str>, config: &Config) -> Result<(), String> { if let Some(name) = profile { if !config.profiles.contains_key(name) { let mut names: Vec<&str> = config.profiles.keys().map(String::as_str).collect(); @@ -130,7 +110,32 @@ fn resolve_repo( )); } } - Ok(repo) + Ok(()) +} + +fn resolve_mono_flags( + mono: MonoRepoFlags, + repo: Option, + default: Option<&ConfigEntry>, +) -> ResolvedMonoFlags { + let deps = mono.deps; + let profile = mono.profile; + let test_repos = mono + .test_repos + .or_else(|| repo.map(|r| vec![r])) + .unwrap_or_default(); + let mono_repo = + mono.mono_repo || deps.is_some() || profile.is_some() || test_repos.len() > 1; + ResolvedMonoFlags { + mono_repo, + mono_dir: mono + .mono_dir + .or_else(|| default.map(|e| e.mono_dir.clone())) + .unwrap_or_else(|| "build-mono".to_string()), + test_repos, + deps, + profile, + } } /// Resolves raw `Args` into `ResolvedArgs` by applying config defaults and CLI overrides. @@ -144,12 +149,13 @@ pub fn resolve_with_config(args: Args, config: &Config) -> Result Args { mono: MonoRepoFlags { mono_repo: false, mono_dir: None, + test_repos: None, deps: None, profile: None, }, diff --git a/tests/config/types.rs b/tests/config/types.rs index 081cee5..8c68cd4 100644 --- a/tests/config/types.rs +++ b/tests/config/types.rs @@ -30,6 +30,7 @@ fn test_from_flags_defaults() { let mono = MonoRepoFlags { mono_repo: false, mono_dir: None, + test_repos: None, deps: None, profile: None, }; @@ -79,6 +80,7 @@ fn test_from_flags_with_values() { let mono = MonoRepoFlags { mono_repo: false, mono_dir: Some("workspace".to_string()), + test_repos: None, deps: None, profile: None, }; diff --git a/tests/interactive.rs b/tests/interactive.rs index 8b993b1..41d4b3b 100644 --- a/tests/interactive.rs +++ b/tests/interactive.rs @@ -17,7 +17,7 @@ fn test_interactive_mode_single_repo() { args }); - assert_eq!(args.repo, Some("user/repo".to_string())); + assert_eq!(args.mono.test_repos, vec!["user/repo".to_string()]); assert!(!args.connection.ssh); assert!(!args.mono.mono_repo); } @@ -68,12 +68,12 @@ fn test_interactive_mode_skips_repo_prompt_when_set() { let input = input_with_suffix(b"n\nn\nn\nn\n1"); let (args, _) = with_io_input_output(&input, |io| { let mut args = default_resolved(); - args.repo = Some("already/set".to_string()); + args.mono.test_repos = vec!["already/set".to_string()]; interactive_mode(&mut args, io).unwrap(); args }); - assert_eq!(args.repo, Some("already/set".to_string())); + assert_eq!(args.mono.test_repos, vec!["already/set".to_string()]); } #[test] @@ -130,7 +130,7 @@ fn test_interactive_mode_invalid_mono_choice_then_valid() { fn test_interactive_mode_errors_on_eof() { let (result, _) = with_io_input_output(b"", |io| { let mut args = default_resolved(); - args.repo = None; + args.mono.test_repos.clear(); interactive_mode(&mut args, io) }); From 8c1f484e1576ec457671664ca4b10872ac6b4268 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Tue, 11 Aug 2026 21:01:22 -0400 Subject: [PATCH 2/4] chore: cargo fmt --- src/interactive.rs | 7 ++++++- src/profile/display.rs | 6 +----- src/resolve/resolver.rs | 3 +-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/interactive.rs b/src/interactive.rs index b275d62..a50398e 100644 --- a/src/interactive.rs +++ b/src/interactive.rs @@ -39,7 +39,12 @@ pub fn interactive_mode(args: &mut ResolvedArgs, io: &mut IoCtx<'_>) -> Result<( if args.mono.mono_repo && args.mono.profile.is_none() && args.mono.deps.is_none() { loop { - match ask(" Mono-repo: (1) Use profile (2) Manual dependency list", io)?.as_str() { + match ask( + " Mono-repo: (1) Use profile (2) Manual dependency list", + io, + )? + .as_str() + { "1" => { args.mono.profile = Some(ask_required(" Profile name", io)?); break; diff --git a/src/profile/display.rs b/src/profile/display.rs index f2ec833..fca54a8 100644 --- a/src/profile/display.rs +++ b/src/profile/display.rs @@ -1,11 +1,7 @@ use crate::{config::Config, ctx::IoCtx, profile::Profile}; use std::io::Write; -pub fn print_profile_details( - output: &mut (impl Write + ?Sized), - title: &str, - profile: &Profile, -) { +pub fn print_profile_details(output: &mut (impl Write + ?Sized), title: &str, profile: &Profile) { let Profile { test_repos, deps } = profile; writeln!(output, " {title}").ok(); for (key, repo) in test_repos { diff --git a/src/resolve/resolver.rs b/src/resolve/resolver.rs index 28abe91..c2e0a33 100644 --- a/src/resolve/resolver.rs +++ b/src/resolve/resolver.rs @@ -124,8 +124,7 @@ fn resolve_mono_flags( .test_repos .or_else(|| repo.map(|r| vec![r])) .unwrap_or_default(); - let mono_repo = - mono.mono_repo || deps.is_some() || profile.is_some() || test_repos.len() > 1; + let mono_repo = mono.mono_repo || deps.is_some() || profile.is_some() || test_repos.len() > 1; ResolvedMonoFlags { mono_repo, mono_dir: mono From 88173cc1356384215f8767ce8681d603529ad1ac Mon Sep 17 00:00:00 2001 From: Masonlet Date: Tue, 11 Aug 2026 22:03:22 -0400 Subject: [PATCH 3/4] docs: document --test-repos --- README.md | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 187638f..4d8d705 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,9 @@ star-setup username/repo # Mono-repo mode star-setup username/repo --deps user/lib1 user/lib2 + +# Multiple test repositories +star-setup --test-repos user/app1 user/app2 --deps user/lib1 ``` ## Prerequisites @@ -92,11 +95,13 @@ cargo install --git https://github.com/star-setup/core | `--no-dev` | Skip opening the dev server (npm) | #### Mono-Repo -| Flag | Description | -|------ |------------- | -| `--deps ...` | List of dependency repositories | -| `--mono-dir ` | Workspace directory (default: `build-mono`) | -| `--profile ` | Use a saved profile | +| Flag | Description | +|------ |------------- | +| `--test-repos ...` | Test repositories (overrides a profile's) | +| `--deps ...` | List of dependency repositories | +| `--mono-dir ` | Workspace directory (default: `build-mono`) | +| `--profile ` | Use a saved profile | +| `--mono-repo` | Force workspace mode for a single test repo | #### Diagnostic | Flag | Description | @@ -113,9 +118,10 @@ Running `star-setup` without arguments launches interactive mode, guiding you th ``` Star Setup Interactive Mode -Enter repository (user/repo or URL): user/repo +Enter test repositories (space separated user/repo or URL): user/repo Use SSH? (y/n) [N]: Verbose? (y/n) [N]: +Show timing? (y/n) [N]: Clean build directory if exists? (y/n) [N]: Select mode: (1) Single Repo (2) Mono-Repo: 1 Build type [Debug]: @@ -136,12 +142,15 @@ star-setup username/repo ``` ### Mono-Repo Mode -Clones multiple repositories into a single workspace and auto-detects the build system. A profile can hold several test repos and dependencies. +Clones multiple repositories into a single workspace and auto-detects the build system. A profile can hold several test repos and dependencies. The positional argument is shorthand for a single test repository, so it cannot be combined with `--test-repos`. One test repository with no dependencies or profile runs in single-repo mode; two or more use a workspace. ```bash # Clone and build a test repo and a list of dependencies star-setup username/repo --deps user/lib1 user/lib2 +# Clone and build several test repositories +star-setup --test-repos user/app1 user/app2 --deps user/lib1 + # Clone and build every test repo + dependency on a saved profile star-setup --profile myprofile ``` @@ -211,7 +220,7 @@ build-mono/ └── user-lib2/ ``` -Watch/dev scripts are generated by default and run each repo's `watch`/`dev` script if present, falling back to `build -- --watch` / `--dev`. Use `--watch`/`--dev` to open them automatically in new terminals, or `--no-watch`/`--no-dev` to skip generation entirely. +Watch/dev scripts are generated by default and run each repo's `watch`/`dev` script if present; watch falls back to `build -- --watch`, and dev falls back to `vercel dev` for Vercel-style repos, or is skipped when neither is found. ```bash # Generate workspace and open watchers @@ -283,7 +292,7 @@ star-setup profile add myprofile --test-repos user/app --deps user/lib1 user/lib # Add a profile with test repos and shared dependencies star-setup profile add myprofile --test-repos user/game1 user/game2 --deps user/lib1 user/lib2 -# Add a dependency-only profile +# Add a dependency-only profile (requires test repos at run time with --test-repos) star-setup profile add myprofile --deps user/lib1 user/lib2 # List profiles @@ -294,6 +303,9 @@ star-setup profile remove myprofile # Use a profile star-setup --profile myprofile + +# Override a profile's test repos with your custom test repositories +star-setup --profile myprofile --test-repos user/other ``` Profiles are stored in `.star-setup.json`: From 28ba03f43b66d1671b8080dc1cc13390031e7856 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Tue, 11 Aug 2026 22:03:37 -0400 Subject: [PATCH 4/4] chore: bump v0.6.3 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 133ed4b..03c1671 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "star-setup" -version = "0.6.2" +version = "0.6.3" edition = "2021" repository = "https://github.com/star-setup/core" description = "Lightweight CLI to clone, configure, and wire single or multi-repo ecosystems"