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.5.0"
version = "0.6.0"
edition = "2021"
repository = "https://github.com/star-setup/core"
description = "Lightweight CLI to clone, configure, and wire single or multi-repo ecosystems"
Expand Down
47 changes: 32 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,17 +136,14 @@ star-setup username/repo
```

### Mono-Repo Mode
Clones multiple repositories into a single workspace and auto-detects the build system.
Clones multiple repositories into a single workspace and auto-detects the build system. A profile can hold several test repos and dependencies.

```bash
# Clone and build a test repo and a manual repo list
star-setup username/repo --repos user/lib1 user/lib2

# Clone and build a saved profile
# Clone and build every test repo + dependency on a saved profile
star-setup --profile myprofile

# Override a profile's test repo
star-setup username/repo --profile myprofile
```

#### Workspace Structures
Expand Down Expand Up @@ -206,13 +203,15 @@ build-mono/
├── package.json # Auto-generated workspace root with workspaces + overrides
├── watch.ps1 # Auto-generated watch script (Windows)
├── watch.sh # Auto-generated watch script (Linux/macOS)
├── dev.ps1 # Auto-generated dev-server script (Windows)
├── dev.sh # Auto-generated dev-server script (Linux/macOS)
└── repos/
├── user-my-repo/ # Test repository
├── user-lib1/
└── user-lib2/
```

Watch scripts are generated by default and run each lib's `watch` script if present, falling back to `build -- --watch`. Use `--watch` to open them automatically in new terminals, or `--no-watch` to skip generation entirely.
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.

```bash
# Generate workspace and open watchers
Expand All @@ -225,12 +224,7 @@ star-setup username/repo --repos user/lib1 user/lib2
star-setup username/repo --repos user/lib1 user/lib2 --no-watch
```

After setup pass `--dev` to launch it automatically: falls back to `vercel dev` if the test repo has no `dev` script but looks like a Vercel project. Or, run it manually from its repo directory:
```bash
cd build-mono/repos/user-my-repo
# npm run dev
# vercel dev
```
Rerun `dev.ps1` / `dev.sh` anytime to reopen every dev server without rebuilding.

</details>

Expand Down Expand Up @@ -283,11 +277,14 @@ star-setup username/repo --config myconfig
### Profile Mode
Profiles represent a saved ecosystem of libraries commonly used together.
```bash
# Add a profile with a test repo and dependencies
star-setup profile add myprofile --test-repo user/app user/lib1 user/lib2
# Add a single-game profile
star-setup profile add myprofile --test-repos user/app --deps user/lib1 user/lib2

# 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
star-setup profile add myprofile user/lib1 user/lib2
star-setup profile add myprofile --deps user/lib1 user/lib2

# List profiles
star-setup profile list
Expand All @@ -299,6 +296,26 @@ star-setup profile remove myprofile
star-setup --profile myprofile
```

Profiles are stored in `.star-setup.json`:
```json
{
"profiles": {
"myprofile": {
"test_repos": {
"user-game1": "user/game1",
"user-game2": "user/game2"
},
"deps": {
"default": [
"user/lib1",
"user/lib2"
]
}
}
}
}
```

### Development
```bash
git clone https://github.com/star-setup/core
Expand Down
5 changes: 3 additions & 2 deletions src/build/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub use meson::{
};
pub mod npm;
pub use npm::{
create_mono_repo_package_json, generate_watch_scripts, maybe_open_dev_server, npm_build,
open_dev_server, open_watch_scripts, read_package_json, resolve_dev_command,
create_mono_repo_package_json, generate_dev_scripts, generate_terminal_scripts,
generate_watch_scripts, maybe_open_dev_server, npm_build, open_dev_server, open_scripts,
read_package_json, resolve_dev_command,
};
75 changes: 59 additions & 16 deletions src/build/npm/dev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
use crate::utils::process::resolve_exe_args;
use crate::{
build::{
read_package_json,
generate_terminal_scripts, read_package_json,
BuildSystem::{self, Npm},
},
ctx::{IoCtx, RunCtx, RunFlags},
repository::repo_dir_name,
resolve::ResolvedArgs,
};
use serde_json::Value;
use std::{
path::Path,
path::{Path, PathBuf},
process::{Command, Stdio},
};

Expand Down Expand Up @@ -41,9 +42,9 @@ fn is_vercel_project(repo_path: &Path, json: &Value) -> bool {
.any(|k| json.get(k).and_then(|d| d.get("@vercel/node")).is_some())
}

/// Runs the dev server in the foreground, blocking until it exits (Ctrl-C).
/// Opens the dev server in a new terminal.
/// # Errors
/// Returns an error only if the process fails to spawn.
/// Returns an error if the terminal cannot be opened.
pub fn open_dev_server(
repo_path: &Path,
cmd: &str,
Expand All @@ -65,8 +66,19 @@ pub fn open_dev_server(

#[cfg(target_os = "windows")]
let mut command = {
let cmd_esc = cmd.replace('\'', "''");
let dir_esc = repo_path.display().to_string().replace('\'', "''");
let mut c = Command::new("powershell");
c.args(["-NoProfile", "-Command", cmd]);
c.args([
"-NoProfile",
"-Command",
"Start-Process",
"powershell",
"-ArgumentList",
&format!("'-NoExit','-Command','{cmd_esc}'"),
"-WorkingDirectory",
&format!("'{dir_esc}'"),
]);
c
};
#[cfg(not(target_os = "windows"))]
Expand All @@ -75,22 +87,19 @@ pub fn open_dev_server(
let resolved = resolve_exe_args(&parts);
let mut c = Command::new(resolved[0]);
c.args(&resolved[1..]);
c.current_dir(repo_path);
c
};

command
.current_dir(repo_path)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());

match command.spawn() {
Err(e) => Err(format!("Failed to start dev server: {e}")),
Ok(mut child) => {
child.wait().ok();
Ok(())
}
}
command
.spawn()
.map(|_| ())
.map_err(|e| format!("Failed to start dev server: {e}"))
}

/// Opens the project's dev server if `--dev` was passed and the build system is npm.
Expand All @@ -115,3 +124,37 @@ pub fn maybe_open_dev_server(
}
Ok(())
}

/// Generates `dev.ps1` / `dev.sh` launching each test repo's dev server in its
/// own terminal, so the whole set can be reopened by rerunning the script.
/// # Errors
/// Returns an error if the scripts cannot be written.
pub fn generate_dev_scripts(
mono_dir: &Path,
repos_path: &Path,
test_repos: &[String],
io: &mut IoCtx<'_>,
flags: RunFlags,
) -> Result<bool, String> {
if test_repos.is_empty() {
return Ok(false);
}
let entries: Vec<(PathBuf, String)> = test_repos
.iter()
.filter_map(|r| {
let dir = repo_dir_name(r);
resolve_dev_command(&repos_path.join(&dir), io, flags)
.map(|cmd| (mono_dir.join("repos").join(&dir), cmd))
})
.collect();

generate_terminal_scripts(
"dev",
"Run all test repositories",
mono_dir,
&entries,
io,
flags,
)?;
Ok(true)
}
6 changes: 4 additions & 2 deletions src/build/npm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ pub mod build;
pub use build::npm_build;
pub mod config;
pub use config::create_mono_repo_package_json;
pub mod scripts;
pub use scripts::{generate_terminal_scripts, open_scripts};
pub mod watch;
pub use watch::{generate_watch_scripts, open_watch_scripts};
pub use watch::generate_watch_scripts;
pub mod dev;
pub use dev::{maybe_open_dev_server, open_dev_server, resolve_dev_command};
pub use dev::{generate_dev_scripts, maybe_open_dev_server, open_dev_server, resolve_dev_command};
pub mod package;
pub use package::read_package_json;
113 changes: 113 additions & 0 deletions src/build/npm/scripts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
use crate::{
ctx::{IoCtx, RunFlags},
utils::{dry_run_or_do, report_summary},
};
use std::{
fs::write,
path::{Path, PathBuf},
process::Command,
};

/// Writes `<name>.ps1` / `<name>.sh` launching each `(dir, cmd)` entry in its own
/// terminal, so the set can be reopened later by rerunning the script.
/// # Errors
/// Returns an error if the scripts cannot be written.
pub fn generate_terminal_scripts(
name: &str,
header: &str,
mono_dir: &Path,
entries: &[(PathBuf, String)],
io: &mut IoCtx<'_>,
flags: RunFlags,
) -> Result<(), String> {
let ps1_lines: Vec<String> = entries
.iter()
.map(|(dir, cmd)| {
format!(
"Start-Process powershell -ArgumentList '-NoExit', '-Command', 'cd \"{}\"; {cmd}'",
dir.display()
)
})
.collect();
let sh_lines: Vec<String> = entries
.iter()
.map(|(dir, cmd)| format!("cd \"{}\" && {cmd} &", dir.display()))
.collect();

let ps1_content = format!("# {header}\n{}\n", ps1_lines.join("\n"));
let sh_content = format!(
"#!/bin/bash\ntrap 'kill $(jobs -p)' EXIT\n# {header}\n{}\nwait\n",
sh_lines.join("\n")
);

let ps1_name = format!("{name}.ps1");
let sh_name = format!("{name}.sh");
dry_run_or_do(
&format!("write {name} scripts"),
"Writing",
mono_dir,
io,
flags,
"Write scripts",
|| {
write(mono_dir.join(&ps1_name), ps1_content)
.map_err(|e| format!("Failed to write {ps1_name}: {e}"))?;
write(mono_dir.join(&sh_name), sh_content)
.map_err(|e| format!("Failed to write {sh_name}: {e}"))?;
Ok(())
},
)?;

report_summary(
io,
flags,
&format!("generate {name} scripts at {}", mono_dir.display()),
&format!("Generated {name} scripts at {}", mono_dir.display()),
);
Ok(())
}

/// Opens the generated `<name>` scripts in new terminals.
/// # Errors
/// Returns an error if the terminal cannot be opened.
pub fn open_scripts(
name: &str,
mono_dir: &Path,
io: &mut IoCtx<'_>,
flags: RunFlags,
) -> Result<(), String> {
if flags.dry_run {
if flags.verbose {
writeln!(io.output, " Would open {name} scripts").ok();
}
return Ok(());
}

crate::time!(flags.timing, io.output, "Open", {
#[cfg(target_os = "windows")]
{
let ps1_path = mono_dir.join(format!("{name}.ps1"));
Command::new("powershell")
.args([
"-ExecutionPolicy",
"Bypass",
"-File",
ps1_path.to_str().ok_or("Invalid path")?,
])
.spawn()
.map_err(|e| format!("Failed to open {name}.ps1: {e}"))?;
}

#[cfg(not(target_os = "windows"))]
{
let sh_path = mono_dir.join(format!("{name}.sh"));
Command::new("bash")
.arg(sh_path.to_str().ok_or("Invalid path")?)
.spawn()
.map_err(|e| format!("Failed to open {name}.sh: {e}"))?;
}
Ok::<(), String>(())
})?;
writeln!(io.output, " Opening {name} scripts").ok();
Ok(())
}
Loading
Loading