Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
30 changes: 21 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <DEPS>...` | List of dependency repositories |
| `--mono-dir <DIR>` | Workspace directory (default: `build-mono`) |
| `--profile <NAME>` | Use a saved profile |
| Flag | Description |
|------ |------------- |
| `--test-repos <TEST_REPOS>...` | Test repositories (overrides a profile's) |
| `--deps <DEPS>...` | List of dependency repositories |
| `--mono-dir <DIR>` | Workspace directory (default: `build-mono`) |
| `--profile <NAME>` | Use a saved profile |
| `--mono-repo` | Force workspace mode for a single test repo |

#### Diagnostic
| Flag | Description |
Expand All @@ -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]:
Expand All @@ -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
```
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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`:
Expand Down
4 changes: 2 additions & 2 deletions src/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,

/// Select a named configuration to use
Expand Down
1 change: 1 addition & 0 deletions src/cli/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 6 additions & 2 deletions src/cli/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,

/// 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<Vec<String>>,

/// List of library dependencies to clone
#[arg(long, num_args = 1.., conflicts_with = "profile")]
pub deps: Option<Vec<String>>,

Expand Down
13 changes: 9 additions & 4 deletions src/commands/mono/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,22 @@ pub fn resolve_test_repos_for_mono(
args: &ResolvedArgs,
profile: Option<&Profile>,
) -> Result<Vec<String>, 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
.values()
.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.
Expand Down
5 changes: 3 additions & 2 deletions src/commands/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
15 changes: 12 additions & 3 deletions src/interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand All @@ -35,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;
Expand Down
6 changes: 1 addition & 5 deletions src/profile/display.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion src/resolve/resolved.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ pub struct ResolvedBuildFlags {
pub struct ResolvedMonoFlags {
pub mono_repo: bool,
pub mono_dir: String,
pub test_repos: Vec<String>,
pub deps: Option<Vec<String>>,
pub profile: Option<String>,
}

/// Fully resolved arguments ready for command execution.
#[derive(Debug)]
pub struct ResolvedArgs {
pub repo: Option<String>,
pub yes: bool,
pub connection: ResolvedConnectionFlags,
pub diagnostic: RunFlags,
Expand Down
55 changes: 30 additions & 25 deletions src/resolve/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
profile: Option<&str>,
config: &Config,
) -> Result<Option<String>, 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();
Expand All @@ -130,7 +110,31 @@ fn resolve_repo(
));
}
}
Ok(repo)
Ok(())
}

fn resolve_mono_flags(
mono: MonoRepoFlags,
repo: Option<String>,
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.
Expand All @@ -144,12 +148,13 @@ pub fn resolve_with_config(args: Args, config: &Config) -> Result<ResolvedArgs,
return Err(format!("Configuration '{config_name}' not found"));
}

validate_profile(args.mono.profile.as_deref(), config)?;

Ok(ResolvedArgs {
repo: resolve_repo(args.repo, args.mono.profile.as_deref(), config)?,
yes: args.yes,
connection: resolve_connection_flags(&args.connection, default),
diagnostic: resolve_run_flags(&args.diagnostic, default),
build: resolve_build_flags(args.build, default)?,
mono: resolve_mono_flags(args.mono, default),
mono: resolve_mono_flags(args.mono, args.repo, default),
})
}
2 changes: 1 addition & 1 deletion src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ fn execute(
return Ok(());
}

let has_repo = args.repo.is_some()
let has_repo = !args.mono.test_repos.is_empty()
|| args.mono.profile.as_deref().is_some_and(|p| {
config
.profiles
Expand Down
2 changes: 1 addition & 1 deletion tests/commands/mono/mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ fn test_mono_repo_mode_multiple_test_repos_opens_all() {
},
);
let mut args = default_resolved_mono(vec![]);
args.repo = None;
args.mono.test_repos.clear();
args.mono.profile = Some("multi".to_string());

let (_, output) = with_ctx(MockRunner::new(), |tmp_path, ctx| {
Expand Down
5 changes: 3 additions & 2 deletions tests/commands/mono/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ fn test_resolve_test_repo_errors() {
#[test]
fn test_resolve_test_repos_for_mono_errors_when_no_repo() {
let mut args = default_resolved();
args.repo = None;
args.mono.test_repos.clear();
assert!(resolve_test_repos_for_mono(&args, None).is_err());
}

Expand All @@ -68,7 +68,8 @@ fn test_resolve_test_repos_for_mono_from_profile() {
]),
deps: BTreeMap::new(),
};
let args = default_resolved();
let mut args = default_resolved();
args.mono.test_repos.clear();
assert_eq!(
resolve_test_repos_for_mono(&args, Some(&profile)),
Ok(vec!["user/game1".to_string(), "user/game2".to_string()])
Expand Down
1 change: 1 addition & 0 deletions tests/common/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub fn default_args() -> Args {
mono: MonoRepoFlags {
mono_repo: false,
mono_dir: None,
test_repos: None,
deps: None,
profile: None,
},
Expand Down
2 changes: 2 additions & 0 deletions tests/config/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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,
};
Expand Down
Loading
Loading